fix(scale): page mail and batch roles
CI and Deploy / test (pull_request) Failing after 2m46s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 21:02:50 +02:00
parent 8ef8b098c8
commit e7cacad7d6
11 changed files with 284 additions and 20 deletions
@@ -85,4 +85,45 @@ public sealed class CorrespondenceControllerTests
var otherResult = await new CorrespondenceController(otherDb).GetMessage(messageId, CancellationToken.None);
Assert.IsType<NotFoundResult>(otherResult.Result);
}
[Fact]
public async Task Paged_inbox_returns_every_owned_message_without_the_legacy_cap()
{
await using var db = TestHostFactory.CreateInMemoryDb();
var company = new Company { Name = "Scale fixture", OwnerUserId = "user-1" };
var otherCompany = new Company { Name = "Other tenant", OwnerUserId = "user-2" };
db.Companies.AddRange(company, otherCompany);
await db.SaveChangesAsync();
var job = new JobApplication { JobTitle = "Paged role", CompanyId = company.Id, OwnerUserId = "user-1" };
var otherJob = new JobApplication { JobTitle = "Private role", CompanyId = otherCompany.Id, OwnerUserId = "user-2" };
db.JobApplications.AddRange(job, otherJob);
await db.SaveChangesAsync();
db.Correspondences.AddRange(Enumerable.Range(1, 205).Select(index => new Correspondence
{
JobApplicationId = job.Id,
From = "Recruiter",
Subject = $"Message {index}",
Content = $"Content {index}",
Date = new DateTime(2026, 1, 1).AddMinutes(index)
}));
db.Correspondences.Add(new Correspondence
{
JobApplicationId = otherJob.Id,
From = "Other recruiter",
Subject = "Private message",
Content = "Must stay tenant isolated",
Date = new DateTime(2026, 2, 1)
});
await db.SaveChangesAsync();
var result = await new CorrespondenceController(db).GetInboxPage(null, null, null, 3, 100, CancellationToken.None);
var page = Assert.IsType<CorrespondenceController.CorrespondenceInboxPageDto>(Assert.IsType<OkObjectResult>(result.Result).Value);
Assert.Equal(205, page.Total);
Assert.Equal(3, page.TotalPages);
Assert.Equal(3, page.Page);
Assert.Equal(5, page.Items.Count);
Assert.Equal("Message 5", page.Items[0].Subject);
Assert.Equal("Message 1", page.Items[^1].Subject);
}
}
+61 -2
View File
@@ -1,5 +1,6 @@
using System.Security.Claims;
using System.Linq.Expressions;
using System.Data.Common;
using JobTrackerApi.Controllers;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
@@ -11,6 +12,10 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Data.Sqlite;
using JobTrackerApi.Data;
using Moq;
using Xunit;
@@ -102,6 +107,42 @@ public sealed class UsersControllerTests
Assert.True(Assert.Single(rows, row => row.Id == member.Id).CanRemoveAdmin);
}
[Fact]
public async Task List_uses_a_fixed_number_of_role_queries_for_many_users()
{
var counter = new CommandCounter();
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var currentUser = new Mock<ICurrentUserService>();
currentUser.SetupGet(service => service.UserId).Returns("admin-1");
var options = new DbContextOptionsBuilder<JobTrackerContext>()
.UseSqlite(connection)
.AddInterceptors(counter)
.Options;
await using var db = new JobTrackerContext(options, currentUser.Object);
await db.Database.EnsureCreatedAsync();
var adminRole = new IdentityRole("Admin") { Id = "role-admin", NormalizedName = "ADMIN" };
var memberRole = new IdentityRole("Member") { Id = "role-member", NormalizedName = "MEMBER" };
db.Roles.AddRange(adminRole, memberRole);
var usersToAdd = Enumerable.Range(1, 75).Select(index => User(index == 1 ? "admin-1" : $"user-{index}")).ToList();
db.Users.AddRange(usersToAdd);
db.UserRoles.Add(new IdentityUserRole<string> { UserId = "admin-1", RoleId = adminRole.Id });
foreach (var user in usersToAdd.Skip(1))
db.UserRoles.Add(new IdentityUserRole<string> { UserId = user.Id, RoleId = memberRole.Id });
await db.SaveChangesAsync();
counter.Reset();
var manager = TestHostFactory.CreateUserManager();
var controller = CreateController(manager, "admin-1", db);
var action = await controller.List(CancellationToken.None);
var rows = Assert.IsType<List<UsersController.UserDto>>(Assert.IsType<OkObjectResult>(action.Result).Value);
Assert.Equal(75, rows.Count);
Assert.Equal(2, counter.ReaderCount);
manager.Verify(userManager => userManager.GetRolesAsync(It.IsAny<ApplicationUser>()), Times.Never);
manager.Verify(userManager => userManager.GetUsersInRoleAsync(It.IsAny<string>()), Times.Never);
}
private static ApplicationUser User(string id) => new()
{
Id = id,
@@ -109,7 +150,7 @@ public sealed class UsersControllerTests
UserName = $"{id}@example.com"
};
private static UsersController CreateController(Mock<UserManager<ApplicationUser>> users, string currentUserId)
private static UsersController CreateController(Mock<UserManager<ApplicationUser>> users, string currentUserId, JobTrackerContext? db = null)
{
var roleStore = new Mock<IRoleStore<IdentityRole>>();
var roles = new Mock<RoleManager<IdentityRole>>(
@@ -125,7 +166,8 @@ public sealed class UsersControllerTests
Mock.Of<IAppEmailSender>(),
new ConfigurationBuilder().Build(),
new NullLogger<UsersController>(),
ExternalOrigin.Parse("http://localhost:3000", production: false));
ExternalOrigin.Parse("http://localhost:3000", production: false),
db: db);
var identity = new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, currentUserId)], "test");
controller.ControllerContext = new ControllerContext
@@ -135,6 +177,23 @@ public sealed class UsersControllerTests
return controller;
}
private sealed class CommandCounter : DbCommandInterceptor
{
public int ReaderCount { get; private set; }
public void Reset() => ReaderCount = 0;
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
ReaderCount++;
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
}
private sealed class TestAsyncQueryProvider<TEntity>(IQueryProvider inner) : IAsyncQueryProvider
{
public IQueryable CreateQuery(Expression expression) => new TestAsyncEnumerable<TEntity>(expression);