using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Tiku.Domain.Content; using Tiku.Domain.QuestionBanks; using Tiku.Infrastructure.Persistence; namespace Tiku.IntegrationTests; public sealed class PersistenceModelTests { private static readonly DbContextOptions Options = new DbContextOptionsBuilder() .UseNpgsql("Host=localhost;Database=tiku_model_tests;Username=postgres") .Options; [Fact] public void Core_model_contains_expected_tables() { using var context = new TikuDbContext(Options); var tableNames = context.Model.GetEntityTypes() .Select(entity => entity.GetTableName()) .ToHashSet(StringComparer.Ordinal); Assert.Equal(37, tableNames.Count); Assert.Contains("tenants", tableNames); Assert.Contains("tenant_settings", tableNames); Assert.Contains("content_entries", tableNames); Assert.Contains("content_nodes", tableNames); Assert.Contains("question_collections", tableNames); Assert.Contains("practice_blueprints", tableNames); Assert.Contains("vocabulary_units", tableNames); Assert.Contains("vocabulary_words", tableNames); Assert.Contains("user_word_progress", tableNames); Assert.Contains("user_word_favorites", tableNames); Assert.Contains("handbook_subjects", tableNames); Assert.Contains("handbook_chapters", tableNames); Assert.Contains("handbook_entries", tableNames); Assert.Contains("question_type_groups", tableNames); Assert.Contains("subject_shares", tableNames); Assert.Contains("questions", tableNames); Assert.Contains("question_versions", tableNames); Assert.Contains("answer_records", tableNames); } [Fact] public void Content_node_path_is_mapped_to_ltree() { using var context = new TikuDbContext(Options); var path = context.Model.FindEntityType(typeof(ContentNode))! .FindProperty(nameof(ContentNode.Path))!; Assert.Equal("ltree", path.GetColumnType()); } [Fact] public void Collection_relations_include_tenant_in_foreign_keys() { using var context = new TikuDbContext(Options); var foreignKeys = context.Model.FindEntityType(typeof(QuestionCollection))! .GetForeignKeys() .Where(foreignKey => foreignKey.Properties.Count > 1) .Select(foreignKey => foreignKey.Properties .Select(property => property.Name) .ToArray()); Assert.All( foreignKeys, properties => Assert.Contains("TenantId", properties)); } [Theory] [InlineData(nameof(Question.Tags), "'[]'::jsonb")] public void Question_json_properties_are_mapped_to_jsonb( string propertyName, string expectedDefaultValueSql) { using var context = new TikuDbContext(Options); var property = context.Model.FindEntityType(typeof(Question))! .FindProperty(propertyName)!; Assert.Equal("jsonb", property.GetColumnType()); Assert.Equal(expectedDefaultValueSql, property.GetDefaultValueSql()); Assert.Equal(typeof(System.Text.Json.JsonElement), property.ClrType); } [Fact] public void Question_relations_include_tenant_in_foreign_keys() { using var context = new TikuDbContext(Options); var foreignKeys = context.Model.FindEntityType(typeof(Question))! .GetForeignKeys() .Select(foreignKey => foreignKey.Properties .Select(property => property.Name) .ToArray()) .ToArray(); Assert.All( foreignKeys.Where(properties => properties.Length > 1), properties => Assert.Contains("TenantId", properties)); Assert.Contains( foreignKeys, properties => properties.SequenceEqual( ["TenantId", "Id", "CurrentVersionId"])); } [Theory] [InlineData(typeof(VocabularyWord), nameof(VocabularyWord.Tags), "'[]'::jsonb")] [InlineData(typeof(VocabularyWord), nameof(VocabularyWord.Metadata), "'{}'::jsonb")] [InlineData(typeof(HandbookSubject), nameof(HandbookSubject.MajorLegacyIds), "'[]'::jsonb")] [InlineData(typeof(HandbookEntry), nameof(HandbookEntry.Tags), "'[]'::jsonb")] [InlineData(typeof(QuestionTypeGroup), nameof(QuestionTypeGroup.Types), "'[]'::jsonb")] [InlineData(typeof(UserWordProgress), nameof(UserWordProgress.Metadata), "'{}'::jsonb")] public void Study_content_json_properties_are_mapped_to_jsonb( Type entityType, string propertyName, string expectedDefaultValueSql) { using var context = new TikuDbContext(Options); var property = context.Model.FindEntityType(entityType)! .FindProperty(propertyName)!; Assert.Equal("jsonb", property.GetColumnType()); Assert.Equal(expectedDefaultValueSql, property.GetDefaultValueSql()); Assert.Equal(typeof(System.Text.Json.JsonElement), property.ClrType); } [Fact] public void User_word_progress_has_review_constraints_and_precision() { using var context = new TikuDbContext(Options); var entityType = context.GetService() .Model .FindEntityType(typeof(UserWordProgress))!; var checkConstraints = entityType.GetCheckConstraints() .Select(constraint => constraint.Name) .ToHashSet(StringComparer.Ordinal); var easeFactor = entityType.FindProperty(nameof(UserWordProgress.EaseFactor))!; Assert.Equal(4, easeFactor.GetPrecision()); Assert.Equal(2, easeFactor.GetScale()); Assert.Contains("ck_user_word_progress_review_count", checkConstraints); Assert.Contains("ck_user_word_progress_correct_streak", checkConstraints); Assert.Contains("ck_user_word_progress_ease_factor", checkConstraints); } [Fact] public void Study_content_relations_include_tenant_in_foreign_keys() { using var context = new TikuDbContext(Options); var entityTypes = new[] { typeof(VocabularyUnit), typeof(VocabularyWord), typeof(UserWordProgress), typeof(UserWordFavorite), typeof(HandbookSubject), typeof(HandbookChapter), typeof(HandbookEntry), typeof(QuestionTypeGroup), typeof(SubjectShare) }; var compositeForeignKeys = entityTypes .SelectMany(entityType => context.Model.FindEntityType(entityType)! .GetForeignKeys()) .Where(foreignKey => foreignKey.Properties.Count > 1) .Select(foreignKey => foreignKey.Properties .Select(property => property.Name) .ToArray()); Assert.All( compositeForeignKeys, properties => Assert.Contains("TenantId", properties)); } [Fact] public void Word_user_join_tables_have_tenant_scoped_unique_indexes() { using var context = new TikuDbContext(Options); AssertHasUniqueIndex( nameof(UserWordProgress.TenantId), nameof(UserWordProgress.UserId), nameof(UserWordProgress.WordId)); AssertHasUniqueIndex( nameof(UserWordFavorite.TenantId), nameof(UserWordFavorite.UserId), nameof(UserWordFavorite.WordId)); void AssertHasUniqueIndex(params string[] propertyNames) { var indexes = context.Model.FindEntityType(typeof(TEntity))!.GetIndexes(); Assert.Contains( indexes, index => index.IsUnique && index.Properties .Select(property => property.Name) .SequenceEqual(propertyNames)); } } }