158 lines
8.9 KiB
C#
158 lines
8.9 KiB
C#
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
using MySqlConnector;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace JobTrackerApi.MariaDb.Tests;
|
|
|
|
public sealed class StartupAndLockTests(ITestOutputHelper output)
|
|
{
|
|
[Fact]
|
|
public async Task Full_startup_creates_every_table_and_column_required_by_the_persisted_EF_model()
|
|
{
|
|
await using var database = await MariaDbDatabase.CreateAsync();
|
|
await using var db = database.Context();
|
|
await using var connection = new MySqlConnection(database.ConnectionString);
|
|
await connection.OpenAsync();
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = "SELECT TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE();";
|
|
await using var reader = await command.ExecuteReaderAsync();
|
|
var actual = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
while (await reader.ReadAsync()) actual.Add($"{reader.GetString(0)}.{reader.GetString(1)}");
|
|
var missing = db.Model.GetRelationalModel().Tables
|
|
.SelectMany(table => table.Columns.Select(column => $"{table.Name}.{column.Name}"))
|
|
.Where(column => !actual.Contains(column)).Order().ToArray();
|
|
output.WriteLine($"Server: {database.ActualVersion}; actual columns: {actual.Count}; missing ({missing.Length}): {string.Join(", ", missing)}");
|
|
Assert.True(missing.Length == 0, $"Startup omitted persisted EF columns: {string.Join(", ", missing)}");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Fresh_repeated_and_populated_startup_preserve_owned_data_and_complete_migrations()
|
|
{
|
|
await using var database = await MariaDbDatabase.CreateAsync();
|
|
await using (var fresh = database.Context())
|
|
{
|
|
Assert.Equal("Pomelo.EntityFrameworkCore.MySql", fresh.Database.ProviderName);
|
|
Assert.Empty(await fresh.Database.GetPendingMigrationsAsync());
|
|
var applied = (await fresh.Database.GetAppliedMigrationsAsync()).ToArray();
|
|
Assert.Contains("20261023090000_AddJobRevision", applied);
|
|
output.WriteLine($"MariaDB server: {database.ActualVersion}; EF: {typeof(DbContext).Assembly.GetName().Version}; Pomelo: {typeof(MariaDbServerVersion).Assembly.GetName().Version}");
|
|
output.WriteLine($"Applied migrations ({applied.Length}): {string.Join(", ", applied)}");
|
|
fresh.Users.AddRange(
|
|
new ApplicationUser { Id = "rd21-owner", UserName = "owner@example.test", UiLanguage = "nb-NO" },
|
|
new ApplicationUser { Id = "other", UserName = "other@example.test", UiLanguage = "en" });
|
|
fresh.Companies.AddRange(new Company { OwnerUserId = "rd21-owner", Name = "Owned" },
|
|
new Company { OwnerUserId = "other", Name = "Private" });
|
|
await fresh.SaveChangesAsync();
|
|
}
|
|
await database.App.InitializeJobTrackerAsync();
|
|
await database.App.InitializeJobTrackerAsync();
|
|
await using var verify = database.Context();
|
|
Assert.Equal(new[] { "other", "rd21-owner" }, await verify.Users.AsNoTracking().OrderBy(x => x.Id).Select(x => x.Id).ToArrayAsync());
|
|
Assert.Equal("nb-NO", (await verify.Users.AsNoTracking().SingleAsync(x => x.Id == "rd21-owner")).UiLanguage);
|
|
Assert.Equal("Owned", (await verify.Companies.AsNoTracking().SingleAsync()).Name);
|
|
Assert.Equal(2, await verify.Companies.IgnoreQueryFilters().CountAsync());
|
|
Assert.Empty(await verify.Database.GetPendingMigrationsAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Rd21_additive_migration_preserves_existing_job_fields_and_backfills_revision_one()
|
|
{
|
|
await using var database = await MariaDbDatabase.CreateAsync();
|
|
await using var db = database.Context();
|
|
var company = new Company { OwnerUserId = "rd21-owner", Name = "Preserve me" };
|
|
db.Add(company);
|
|
await db.SaveChangesAsync();
|
|
var job = new JobApplication
|
|
{
|
|
OwnerUserId = "rd21-owner", CompanyId = company.Id,
|
|
JobTitle = "Historical title", Notes = "Historical notes", Status = "Prospect",
|
|
CoverLetterText = "Historical letter", TailoredCvText = "Historical CV",
|
|
};
|
|
job.Job = JobOpportunitySync.Create(job);
|
|
db.Add(job);
|
|
await db.SaveChangesAsync();
|
|
var migrations = (await db.Database.GetAppliedMigrationsAsync()).ToArray();
|
|
Assert.Equal("20261023090000_AddJobRevision", migrations[^1]);
|
|
// Only this test's newly-created schema is downgraded; no existing database is selected.
|
|
await db.GetService<IMigrator>().MigrateAsync(migrations[^2]);
|
|
Assert.Equal(new[] { migrations[^1] }, await db.Database.GetPendingMigrationsAsync());
|
|
db.ChangeTracker.Clear();
|
|
await database.App.InitializeJobTrackerAsync();
|
|
await using var verify = database.Context();
|
|
var actual = await verify.JobApplications.AsNoTracking().SingleAsync();
|
|
Assert.Equal(job.Id, actual.Id);
|
|
Assert.Equal(job.CompanyId, actual.CompanyId);
|
|
Assert.Equal("Historical title", actual.JobTitle);
|
|
Assert.Equal("Historical notes", actual.Notes);
|
|
Assert.Equal("Prospect", actual.Status);
|
|
Assert.Equal("Historical letter", actual.CoverLetterText);
|
|
Assert.Equal("Historical CV", actual.TailoredCvText);
|
|
Assert.Equal("rd21-owner", actual.OwnerUserId);
|
|
Assert.Equal(1, actual.Version);
|
|
Assert.Equal(migrations, await verify.Database.GetAppliedMigrationsAsync());
|
|
Assert.Empty(await verify.Database.GetPendingMigrationsAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Open_reader_does_not_block_independent_usage_and_identity_writes()
|
|
{
|
|
await using var database = await MariaDbDatabase.CreateAsync();
|
|
await using (var seed = database.Context())
|
|
{
|
|
seed.Users.AddRange(new ApplicationUser { Id = "rd21-owner", UserName = "owner@example.test", UiLanguage = "en" },
|
|
new ApplicationUser { Id = "other", UserName = "other@example.test", UiLanguage = "en" });
|
|
seed.Companies.AddRange(new Company { OwnerUserId = "rd21-owner", Name = "Owned" },
|
|
new Company { OwnerUserId = "other", Name = "Other" });
|
|
await seed.SaveChangesAsync();
|
|
}
|
|
await using var connection = new MySqlConnection(database.ConnectionString);
|
|
await connection.OpenAsync();
|
|
await using var snapshot = await connection.BeginTransactionAsync(System.Data.IsolationLevel.RepeatableRead);
|
|
await using var command = connection.CreateCommand();
|
|
command.Transaction = snapshot;
|
|
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("rd21-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("rd21-owner"))!;
|
|
owner.UiLanguage = "nb-NO";
|
|
Assert.True((await users.UpdateAsync(owner)).Succeeded);
|
|
await using var verify = database.Context();
|
|
var usage = await verify.AiUsageRecords.AsNoTracking().SingleAsync();
|
|
Assert.Equal(reservation.Record.Id, usage.Id);
|
|
Assert.Equal("held-reader", usage.SourceId);
|
|
Assert.Equal(1, usage.CallCount);
|
|
Assert.Equal("nb-NO", (await verify.Users.AsNoTracking().SingleAsync(x => x.Id == "rd21-owner")).UiLanguage);
|
|
Assert.Equal("en", (await verify.Users.AsNoTracking().SingleAsync(x => x.Id == "other")).UiLanguage);
|
|
Assert.True(await reader.ReadAsync());
|
|
Assert.False(await reader.ReadAsync());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("Server=database.example.test;User ID=test;Password=synthetic")]
|
|
[InlineData("Server=192.0.2.1;Database=development;User ID=test")]
|
|
public void Unsafe_or_missing_admin_configuration_fails_closed(string? connection)
|
|
=> Assert.Throws<InvalidOperationException>(() => MariaDbDatabase.ValidateAdminConnection(connection));
|
|
|
|
[Fact]
|
|
public void Admin_database_name_is_never_used_as_the_test_database()
|
|
{
|
|
var connection = MariaDbDatabase.ValidateAdminConnection("Server=127.0.0.1;Database=development;User ID=test");
|
|
Assert.Equal("", connection.Database);
|
|
Assert.False(connection.Pooling);
|
|
}
|
|
}
|