Files
jobtrackingapp/JobTrackerApi.Tests/SqliteStartupConcurrencyTests.cs
T

143 lines
7.3 KiB
C#

using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class SqliteStartupConcurrencyTests
{
[Fact]
public async Task Fresh_and_populated_startup_enable_WAL_without_changing_owned_rows()
{
await using var database = await Database.CreateAsync();
await using var connection = new SqliteConnection(database.ConnectionString);
await connection.OpenAsync();
await using var journal = connection.CreateCommand();
journal.CommandText = "PRAGMA journal_mode;";
Assert.Equal("wal", await journal.ExecuteScalarAsync());
journal.CommandText = "PRAGMA journal_mode=DELETE;";
Assert.Equal("delete", await journal.ExecuteScalarAsync());
await connection.CloseAsync();
await database.App.InitializeJobTrackerAsync();
await connection.OpenAsync();
journal.CommandText = "PRAGMA journal_mode;";
Assert.Equal("wal", await journal.ExecuteScalarAsync());
await using var scope = database.App.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
Assert.Equal(new[] { "other", "owner" }, await db.Users.OrderBy(user => user.Id).Select(user => user.Id).ToArrayAsync());
Assert.Equal(new[] { "Other company", "Owned company" }, await db.Companies.IgnoreQueryFilters().OrderBy(company => company.Name).Select(company => company.Name).ToArrayAsync());
Assert.All(await db.Users.ToListAsync(), user => Assert.Equal("en", user.UiLanguage));
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
[Fact]
public async Task An_open_read_cursor_does_not_block_usage_reservation_or_Identity_preference_save()
{
await using var database = await Database.CreateAsync();
await using var connection = new SqliteConnection(database.ConnectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT Id FROM Companies ORDER BY Id;";
await using var reader = await command.ExecuteReaderAsync();
Assert.True(await reader.ReadAsync());
await using var scope = database.App.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
var reservation = await new AiUsageMeter(db, TimeProvider.System).ReserveAsync(
"owner", new AccountEntitlements(true, true, 1_000_000, 10, 10_000),
"synchronous", "held-reader", "synchronous.summary", 40, 10, default);
Assert.True(reservation.Created);
var users = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var owner = await users.FindByIdAsync("owner");
Assert.NotNull(owner);
owner.UiLanguage = "nb-NO";
Assert.True((await users.UpdateAsync(owner)).Succeeded);
await using var verifyScope = database.App.Services.CreateAsyncScope();
var verify = verifyScope.ServiceProvider.GetRequiredService<JobTrackerContext>();
var usage = Assert.Single(await verify.AiUsageRecords.AsNoTracking().ToListAsync());
Assert.Equal(reservation.Record.Id, usage.Id);
Assert.Equal("owner", usage.OwnerUserId);
Assert.Equal("held-reader", usage.SourceId);
Assert.Equal(1, usage.CallCount);
Assert.Equal("nb-NO", (await verify.Users.AsNoTracking().SingleAsync(user => user.Id == "owner")).UiLanguage);
Assert.Equal("en", (await verify.Users.AsNoTracking().SingleAsync(user => user.Id == "other")).UiLanguage);
Assert.Equal(2, await verify.Companies.IgnoreQueryFilters().CountAsync());
Assert.True(await reader.ReadAsync());
Assert.False(await reader.ReadAsync());
}
private sealed class Database(WebApplication app, string root, string connectionString) : IAsyncDisposable
{
public WebApplication App { get; } = app;
public string ConnectionString { get; } = connectionString;
public static async Task<Database> CreateAsync()
{
var root = Path.Combine(Path.GetTempPath(), $"jobtracker-startup-lock-{Guid.NewGuid():N}");
var connectionString = $"Data Source={Path.Combine(root, "jobtracker.db")};Default Timeout=1;Pooling=False";
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
EnvironmentName = "Production",
ApplicationName = typeof(StartupInitializationExtensions).Assembly.GetName().Name,
});
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["Data:Root"] = root,
["Database:Provider"] = "sqlite",
["ConnectionStrings:JobTracker"] = connectionString,
["Auth:AdminEmail"] = "",
["Auth:AdminPassword"] = "",
});
builder.Logging.ClearProviders();
var currentUser = new Mock<ICurrentUserService>();
currentUser.SetupGet(service => service.UserId).Returns("owner");
builder.Services.AddSingleton(currentUser.Object);
builder.Services.AddSingleton<AppPaths>();
builder.Services.AddSingleton<IStartupReadiness, StartupReadiness>();
builder.Services.AddDbContext<JobTrackerContext>(options => options.UseSqlite(connectionString)
.ConfigureWarnings(warnings => warnings.Ignore(RelationalEventId.PendingModelChangesWarning)));
builder.Services.AddIdentityCore<ApplicationUser>().AddRoles<IdentityRole>().AddEntityFrameworkStores<JobTrackerContext>();
var app = builder.Build();
var database = new Database(app, root, connectionString);
try
{
await app.InitializeJobTrackerAsync();
await using var scope = app.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
db.Users.AddRange(
new ApplicationUser { Id = "owner", UserName = "owner@example.test", UiLanguage = "en" },
new ApplicationUser { Id = "other", UserName = "other@example.test", UiLanguage = "en" });
db.Companies.AddRange(
new Company { Name = "Owned company", OwnerUserId = "owner" },
new Company { Name = "Other company", OwnerUserId = "other" });
await db.SaveChangesAsync();
return database;
}
catch
{
await database.DisposeAsync();
throw;
}
}
public async ValueTask DisposeAsync()
{
await App.DisposeAsync();
var fullRoot = Path.GetFullPath(root);
Assert.StartsWith(Path.GetFullPath(Path.GetTempPath()), fullRoot, StringComparison.OrdinalIgnoreCase);
Assert.StartsWith("jobtracker-startup-lock-", Path.GetFileName(fullRoot), StringComparison.Ordinal);
if (Directory.Exists(fullRoot)) Directory.Delete(fullRoot, recursive: true);
}
}
}