using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Exams; using Jiaowu.Api.Infrastructure.Teaching; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using MySql.Data.MySqlClient; namespace Jiaowu.Api.Tests; public sealed class MySqlMigrationTests { private const string LatestMigration = "20260726190000_PersonalCalendarSubscription"; [Fact] public void Production_migration_is_discoverable_and_generates_mysql_sql() { var options = CreateMySqlOptions(); using var db = new AppDbContext(options); Assert.Contains(LatestMigration, db.Database.GetMigrations()); var script = db.GetService().GenerateScript( fromMigration: "20260725040310_SchedulePublishJobs", toMigration: LatestMigration); Assert.Contains("CREATE TABLE `GradeItems`", script); Assert.Contains("CREATE TABLE `EvaluationSetups`", script); Assert.Contains("CREATE TABLE `WarningRules`", script); Assert.Contains("ADD `CheckInMethod` int NOT NULL DEFAULT 1", script); Assert.Contains("CREATE UNIQUE INDEX `IX_AttendanceSheets_CheckInToken`", script); Assert.Contains("ADD COLUMN `WaitlistedAt` datetime(6) NULL", script); Assert.Contains( "CREATE INDEX `IX_CE_Offering_Status_WaitlistedAt`", script); Assert.Contains("@waitlisted_column_exists", script); Assert.Contains("@waitlist_index_exists", script); Assert.DoesNotContain( "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt", script); Assert.Contains( "ADD `MaxCourseCount` int NULL", script); Assert.Contains( "CREATE TABLE `CourseSelectionRoundGrades`", script); Assert.Contains( "CREATE UNIQUE INDEX `IX_CourseSelectionRoundGrades_CourseSelectionRoundId_Grade`", script); Assert.Contains( "ADD `CalendarSubscriptionStamp` varchar(64) NULL", script); Assert.Contains( "ADD `CalendarSubscriptionCreatedAt` datetime(6) NULL", script); Assert.Contains("SET `ExamDate` = DATE(`StartsAt`)", script); Assert.Contains("DEFAULT 1", script); Assert.DoesNotContain("0001-01-01", script); Assert.DoesNotContain("0000-00-00", script); } [Fact] public void MySql_date_properties_use_datetime_provider_conversion() { using var db = new AppDbContext(CreateMySqlOptions()); var dateProperties = db.Model.GetEntityTypes() .SelectMany(x => x.GetProperties()) .Where(x => x.ClrType == typeof(DateOnly) || x.ClrType == typeof(DateOnly?)) .ToList(); Assert.NotEmpty(dateProperties); Assert.All(dateProperties, property => { Assert.Equal("date", property.GetColumnType()); Assert.Equal( typeof(DateTime), property.GetTypeMapping().Converter?.ProviderClrType); }); } [Fact] public void MySql_guid_collections_use_provider_safe_predicates() { using var db = new AppDbContext(CreateMySqlOptions()); var ids = new[] { Guid.Parse("11111111-1111-1111-1111-111111111111"), Guid.Parse("22222222-2222-2222-2222-222222222222") }; var sql = db.Colleges .WhereIn(ids, x => x.Id) .ToQueryString(); Assert.Contains(" IN (", sql, StringComparison.OrdinalIgnoreCase); Assert.Contains(ids[0].ToString(), sql, StringComparison.OrdinalIgnoreCase); Assert.Contains(ids[1].ToString(), sql, StringComparison.OrdinalIgnoreCase); } [Fact] public void MySql_invigilator_candidate_query_is_translatable_without_uuid_ordering() { using var db = new AppDbContext(CreateMySqlOptions()); var excludedTeacherId = Guid.Parse("11111111-1111-1111-1111-111111111111"); var sql = InvigilatorCandidateQuery .Create(db, [excludedTeacherId]) .ToQueryString(); Assert.Contains("Teachers", sql, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("UUID(", sql, StringComparison.OrdinalIgnoreCase); } [Fact] public void MySql_teaching_task_roster_query_is_translatable() { using var db = new AppDbContext(CreateMySqlOptions()); var sql = TeachingTaskRosterQuery .ForTask( db, Guid.Parse("11111111-1111-1111-1111-111111111111")) .ToQueryString(); Assert.Contains("TeachingTaskClasses", sql, StringComparison.OrdinalIgnoreCase); Assert.Contains("CourseEnrollments", sql, StringComparison.OrdinalIgnoreCase); } [Fact] public void MySql_index_names_fit_the_server_identifier_limit() { using var db = new AppDbContext(CreateMySqlOptions()); var indexNames = db.Model.GetEntityTypes() .SelectMany(x => x.GetIndexes()) .Select(x => x.GetDatabaseName()) .Where(x => !string.IsNullOrEmpty(x)) .ToList(); Assert.NotEmpty(indexNames); Assert.All(indexNames, name => Assert.True( name!.Length <= 64, $"MySQL index name '{name}' has {name.Length} characters.")); } [Fact] public void Application_mysql_connections_allow_migration_user_variables() { var normalized = MySqlConnectionStringPolicy.ForApplication( "Server=localhost;Database=jiaowu;User=test;Password=test;"); var builder = new MySqlConnectionStringBuilder(normalized); Assert.True(builder.AllowUserVariables); } private static DbContextOptions CreateMySqlOptions() => new DbContextOptionsBuilder() .UseMySQL( "Server=localhost;Database=jiaowu;User=__test__;Password=__not_used__;") .Options; }