143 lines
7.3 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|