Files
tiku-backend.net/Tiku.IntegrationTests/PersistenceModelTests.cs

718 lines
33 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Tiku.Domain.Commerce;
using Tiku.Domain.Content;
using Tiku.Domain.Growth;
using Tiku.Domain.Learning;
using Tiku.Domain.Operations;
using Tiku.Domain.QuestionBanks;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
namespace Tiku.IntegrationTests;
public sealed class PersistenceModelTests
{
private static readonly DbContextOptions<TikuDbContext> Options =
new DbContextOptionsBuilder<TikuDbContext>()
.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(111, 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("content_assets", tableNames);
Assert.Contains("content_import_jobs", tableNames);
Assert.Contains("content_import_items", tableNames);
Assert.Contains("content_import_issues", tableNames);
Assert.Contains("images", tableNames);
Assert.Contains("app_assets", tableNames);
Assert.Contains("video_explanations", tableNames);
Assert.Contains("question_videos", tableNames);
Assert.Contains("exam_dates", tableNames);
Assert.Contains("reports", tableNames);
Assert.Contains("report_status_events", tableNames);
Assert.Contains("user_score_events", tableNames);
Assert.Contains("practice_daily_usage", tableNames);
Assert.Contains("practice_access_events", tableNames);
Assert.Contains("practice_session_reports", tableNames);
Assert.Contains("practice_session_report_sections", tableNames);
Assert.Contains("dashboard_daily_stats", tableNames);
Assert.Contains("revenue_daily_stats", tableNames);
Assert.Contains("tenant_auth_providers", tableNames);
Assert.Contains("sms_verification_codes", tableNames);
Assert.Contains("auth_login_events", tableNames);
Assert.Contains("auth_sessions", tableNames);
Assert.Contains("sms_send_rate_limits", tableNames);
Assert.Contains("tenant_role_templates", tableNames);
Assert.Contains("tenant_classes", tableNames);
Assert.Contains("tenant_class_members", tableNames);
Assert.Contains("tenant_student_notes", tableNames);
Assert.Contains("tenant_student_followups", tableNames);
Assert.Contains("products", tableNames);
Assert.Contains("svip_plans", tableNames);
Assert.Contains("orders", tableNames);
Assert.Contains("order_items", tableNames);
Assert.Contains("payments", tableNames);
Assert.Contains("payment_events", tableNames);
Assert.Contains("entitlements", tableNames);
Assert.Contains("code_batches", tableNames);
Assert.Contains("activation_codes", tableNames);
Assert.Contains("coupons", tableNames);
Assert.Contains("coupon_redemptions", tableNames);
Assert.Contains("tenant_payment_accounts", tableNames);
Assert.Contains("tenant_subscriptions", tableNames);
Assert.Contains("tenant_usage_records", tableNames);
Assert.Contains("commerce_refund_requests", tableNames);
Assert.Contains("commerce_refund_events", tableNames);
Assert.Contains("commerce_reconciliation_batches", tableNames);
Assert.Contains("commerce_reconciliation_items", tableNames);
Assert.Contains("commerce_reconciliation_issues", tableNames);
Assert.Contains("commerce_reconciliation_issue_events", tableNames);
Assert.Contains("content_asset_access_events", tableNames);
Assert.Contains("content_asset_security_scan_events", tableNames);
Assert.Contains("question_bank_grants", tableNames);
Assert.Contains("tenant_question_bank_adoptions", tableNames);
Assert.Contains("ai_recommendation_reports", tableNames);
Assert.Contains("referral_tracks", tableNames);
Assert.Contains("referral_codes", tableNames);
Assert.Contains("referral_leads", tableNames);
Assert.Contains("referral_team_edges", tableNames);
Assert.Contains("referral_qrcodes", tableNames);
Assert.Contains("crm_config", tableNames);
Assert.Contains("crm_webhook_queue", tableNames);
Assert.Contains("crm_webhook_log", tableNames);
Assert.Contains("tenant_commission_settings", tableNames);
Assert.Contains("commission_settlements", tableNames);
Assert.Contains("commission_settlement_items", tableNames);
Assert.Contains("banners", tableNames);
Assert.Contains("faqs", tableNames);
Assert.Contains("announcements", tableNames);
Assert.Contains("audit_logs", tableNames);
Assert.Contains("user_notifications", tableNames);
Assert.Contains("badges", tableNames);
Assert.Contains("user_badges", tableNames);
Assert.Contains("tenant_content_notifications", tableNames);
Assert.Contains("tenant_theme_templates", tableNames);
Assert.Contains("tenant_theme_configs", 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<IDesignTimeModel>()
.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<UserWordProgress>(
nameof(UserWordProgress.TenantId),
nameof(UserWordProgress.UserId),
nameof(UserWordProgress.WordId));
AssertHasUniqueIndex<UserWordFavorite>(
nameof(UserWordFavorite.TenantId),
nameof(UserWordFavorite.UserId),
nameof(UserWordFavorite.WordId));
void AssertHasUniqueIndex<TEntity>(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));
}
}
[Theory]
[InlineData(typeof(ContentAsset), nameof(ContentAsset.AccessRules), "'{}'::jsonb")]
[InlineData(typeof(ContentAsset), nameof(ContentAsset.VerificationDetails), "'{}'::jsonb")]
[InlineData(typeof(ContentAsset), nameof(ContentAsset.SecurityScanSummary), "'{}'::jsonb")]
[InlineData(typeof(ContentAsset), nameof(ContentAsset.SecurityFlags), "'{}'::jsonb")]
[InlineData(typeof(ContentAssetAccessEvent), nameof(ContentAssetAccessEvent.Metadata), "'{}'::jsonb")]
[InlineData(typeof(ContentAssetSecurityScanEvent), nameof(ContentAssetSecurityScanEvent.Details), "'{}'::jsonb")]
[InlineData(typeof(ContentImportJob), nameof(ContentImportJob.RawPayload), "'[]'::jsonb")]
[InlineData(typeof(ContentImportItem), nameof(ContentImportItem.SourcePayload), "'{}'::jsonb")]
[InlineData(typeof(ContentImportIssue), nameof(ContentImportIssue.Details), "'{}'::jsonb")]
[InlineData(typeof(VideoExplanation), nameof(VideoExplanation.KnowledgeTags), "'[]'::jsonb")]
[InlineData(typeof(QuestionVideo), nameof(QuestionVideo.Metadata), "'{}'::jsonb")]
[InlineData(typeof(QuestionBankGrant), nameof(QuestionBankGrant.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantQuestionBankAdoption), nameof(TenantQuestionBankAdoption.SourceSnapshot), "'{}'::jsonb")]
[InlineData(typeof(TenantQuestionBankAdoption), nameof(TenantQuestionBankAdoption.Metadata), "'{}'::jsonb")]
[InlineData(typeof(AiRecommendationReport), nameof(AiRecommendationReport.InputPayload), "'{}'::jsonb")]
[InlineData(typeof(AiRecommendationReport), nameof(AiRecommendationReport.ContextPayload), "'{}'::jsonb")]
[InlineData(typeof(AiRecommendationReport), nameof(AiRecommendationReport.ResultPayload), "'{}'::jsonb")]
public void Asset_import_and_video_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 Import_items_and_question_videos_have_expected_unique_indexes()
{
using var context = new TikuDbContext(Options);
AssertHasUniqueIndex<ContentImportItem>(
nameof(ContentImportItem.JobId),
nameof(ContentImportItem.RowNo));
AssertHasUniqueIndex<QuestionVideo>(
nameof(QuestionVideo.TenantId),
nameof(QuestionVideo.QuestionId),
nameof(QuestionVideo.VideoId),
nameof(QuestionVideo.VideoType));
AssertHasUniqueIndex<TenantQuestionBankAdoption>(
nameof(TenantQuestionBankAdoption.TenantId),
nameof(TenantQuestionBankAdoption.SourceQuestionBankId));
var allowedPlanCodes = context.Model.FindEntityType(typeof(QuestionBankGrant))!
.FindProperty(nameof(QuestionBankGrant.AllowedPlanCodes))!;
Assert.Equal("text[]", allowedPlanCodes.GetColumnType());
var allowedTenantIds = context.Model.FindEntityType(typeof(QuestionBankGrant))!
.FindProperty(nameof(QuestionBankGrant.AllowedTenantIds))!;
Assert.Equal("uuid[]", allowedTenantIds.GetColumnType());
void AssertHasUniqueIndex<TEntity>(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));
}
}
[Fact]
public void Asset_import_and_video_relations_include_tenant_in_foreign_keys()
{
using var context = new TikuDbContext(Options);
var entityTypes = new[]
{
typeof(ContentAsset),
typeof(ContentImportJob),
typeof(ContentImportItem),
typeof(ContentImportIssue),
typeof(VideoExplanation),
typeof(QuestionVideo)
};
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));
}
[Theory]
[InlineData(typeof(ContentNode), nameof(ContentNode.AccessRules), "'{}'::jsonb")]
[InlineData(typeof(QuestionCollection), nameof(QuestionCollection.AccessRules), "'{}'::jsonb")]
[InlineData(typeof(PracticeBlueprint), nameof(PracticeBlueprint.AccessRules), "'{}'::jsonb")]
[InlineData(typeof(PracticeSession), nameof(PracticeSession.AccessSnapshot), "'{}'::jsonb")]
[InlineData(typeof(Report), nameof(Report.Attachments), "'[]'::jsonb")]
[InlineData(typeof(Report), nameof(Report.Metadata), "'{}'::jsonb")]
[InlineData(typeof(PracticeSessionReport), nameof(PracticeSessionReport.QuestionResults), "'[]'::jsonb")]
[InlineData(typeof(PracticeSessionReport), nameof(PracticeSessionReport.WrongQuestionIds), "'[]'::jsonb")]
[InlineData(typeof(PracticeSessionReportSection), nameof(PracticeSessionReportSection.Metadata), "'{}'::jsonb")]
[InlineData(typeof(PracticeDailyUsage), nameof(PracticeDailyUsage.Metadata), "'{}'::jsonb")]
[InlineData(typeof(PracticeAccessEvent), nameof(PracticeAccessEvent.Metadata), "'{}'::jsonb")]
public void Learning_report_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 Practice_usage_unique_scope_treats_nulls_as_not_distinct()
{
using var context = new TikuDbContext(Options);
var index = context.GetService<IDesignTimeModel>()
.Model
.FindEntityType(typeof(PracticeDailyUsage))!
.GetIndexes()
.Single(index => index.IsUnique
&& index.Properties.Select(property => property.Name).SequenceEqual([
nameof(PracticeDailyUsage.TenantId),
nameof(PracticeDailyUsage.UserId),
nameof(PracticeDailyUsage.UsageDate),
nameof(PracticeDailyUsage.ScopeType),
nameof(PracticeDailyUsage.ScopeId)
]));
Assert.False(index.GetAreNullsDistinct());
}
[Fact]
public void Practice_report_scores_have_expected_precision()
{
using var context = new TikuDbContext(Options);
var reportType = context.Model.FindEntityType(typeof(PracticeSessionReport))!;
var score = reportType.FindProperty(nameof(PracticeSessionReport.Score))!;
var accuracy = reportType.FindProperty(nameof(PracticeSessionReport.Accuracy))!;
Assert.Equal(10, score.GetPrecision());
Assert.Equal(2, score.GetScale());
Assert.Equal(6, accuracy.GetPrecision());
Assert.Equal(4, accuracy.GetScale());
}
[Theory]
[InlineData(typeof(TenantAuthProvider), nameof(TenantAuthProvider.ConfigPublic), "'{}'::jsonb")]
[InlineData(typeof(SmsVerificationCode), nameof(SmsVerificationCode.Metadata), "'{}'::jsonb")]
[InlineData(typeof(AuthLoginEvent), nameof(AuthLoginEvent.Metadata), "'{}'::jsonb")]
[InlineData(typeof(AuthSession), nameof(AuthSession.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantRoleTemplate), nameof(TenantRoleTemplate.Permissions), "'{}'::jsonb")]
[InlineData(typeof(TenantRoleTemplate), nameof(TenantRoleTemplate.DataScope), "'{}'::jsonb")]
[InlineData(typeof(TenantClass), nameof(TenantClass.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantClassMember), nameof(TenantClassMember.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantStudentNote), nameof(TenantStudentNote.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantStudentFollowup), nameof(TenantStudentFollowup.Metadata), "'{}'::jsonb")]
public void Auth_and_tenant_operations_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 Auth_and_class_tables_have_expected_unique_indexes()
{
using var context = new TikuDbContext(Options);
AssertHasUniqueIndex<TenantAuthProvider>(
nameof(TenantAuthProvider.TenantId),
nameof(TenantAuthProvider.Provider));
AssertHasUniqueIndex<TenantClassMember>(
nameof(TenantClassMember.TenantId),
nameof(TenantClassMember.ClassId),
nameof(TenantClassMember.UserId),
nameof(TenantClassMember.MemberType));
void AssertHasUniqueIndex<TEntity>(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));
}
}
[Theory]
[InlineData(typeof(Product), nameof(Product.Tags), "'[]'::jsonb")]
[InlineData(typeof(Product), nameof(Product.DetailImages), "'[]'::jsonb")]
[InlineData(typeof(Order), nameof(Order.RawPayload), "'{}'::jsonb")]
[InlineData(typeof(OrderItem), nameof(OrderItem.Metadata), "'{}'::jsonb")]
[InlineData(typeof(Payment), nameof(Payment.RawPayload), "'{}'::jsonb")]
[InlineData(typeof(PaymentEvent), nameof(PaymentEvent.Payload), "'{}'::jsonb")]
[InlineData(typeof(Entitlement), nameof(Entitlement.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantPaymentAccount), nameof(TenantPaymentAccount.ConfigPublic), "'{}'::jsonb")]
[InlineData(typeof(TenantSubscription), nameof(TenantSubscription.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantUsageRecord), nameof(TenantUsageRecord.Metadata), "'{}'::jsonb")]
[InlineData(typeof(CommerceRefundRequest), nameof(CommerceRefundRequest.Metadata), "'{}'::jsonb")]
[InlineData(typeof(CommerceRefundEvent), nameof(CommerceRefundEvent.Details), "'{}'::jsonb")]
[InlineData(typeof(CommerceReconciliationBatch), nameof(CommerceReconciliationBatch.Metadata), "'{}'::jsonb")]
[InlineData(typeof(CommerceReconciliationItem), nameof(CommerceReconciliationItem.Details), "'{}'::jsonb")]
[InlineData(typeof(CommerceReconciliationIssue), nameof(CommerceReconciliationIssue.Metadata), "'{}'::jsonb")]
[InlineData(typeof(CommerceReconciliationIssueEvent), nameof(CommerceReconciliationIssueEvent.Details), "'{}'::jsonb")]
public void Commerce_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 Commerce_tables_have_expected_unique_indexes_and_money_types()
{
using var context = new TikuDbContext(Options);
AssertHasUniqueIndex<Order>(nameof(Order.TenantId), nameof(Order.OrderNo));
AssertHasUniqueIndex<ActivationCode>(nameof(ActivationCode.TenantId), nameof(ActivationCode.Code));
AssertHasUniqueIndex<TenantPaymentAccount>(
nameof(TenantPaymentAccount.TenantId),
nameof(TenantPaymentAccount.Provider));
AssertHasUniqueIndex<CommerceRefundRequest>(
nameof(CommerceRefundRequest.TenantId),
nameof(CommerceRefundRequest.RefundNo));
AssertHasUniqueIndex<CommerceReconciliationItem>(
nameof(CommerceReconciliationItem.BatchId),
nameof(CommerceReconciliationItem.RowNo));
AssertHasUniqueIndex<CommerceReconciliationIssue>(
nameof(CommerceReconciliationIssue.TenantId),
nameof(CommerceReconciliationIssue.IssueNo));
AssertHasUniqueIndex<CommerceReconciliationIssue>(
nameof(CommerceReconciliationIssue.TenantId),
nameof(CommerceReconciliationIssue.ItemId));
var amount = context.Model.FindEntityType(typeof(Order))!
.FindProperty(nameof(Order.AmountCents))!;
Assert.Equal(typeof(int), amount.ClrType);
var refundedAmount = context.Model.FindEntityType(typeof(Order))!
.FindProperty(nameof(Order.RefundedAmountCents))!;
Assert.Equal(typeof(int), refundedAmount.ClrType);
void AssertHasUniqueIndex<TEntity>(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));
}
}
[Theory]
[InlineData(typeof(ReferralTrack), nameof(ReferralTrack.Metadata), "'{}'::jsonb")]
[InlineData(typeof(ReferralCode), nameof(ReferralCode.Metadata), "'{}'::jsonb")]
[InlineData(typeof(ReferralLead), nameof(ReferralLead.Metadata), "'{}'::jsonb")]
[InlineData(typeof(ReferralTeamEdge), nameof(ReferralTeamEdge.Metadata), "'{}'::jsonb")]
[InlineData(typeof(ReferralQrcode), nameof(ReferralQrcode.Metadata), "'{}'::jsonb")]
[InlineData(typeof(CrmConfig), nameof(CrmConfig.AssignmentPool), "'[]'::jsonb")]
[InlineData(typeof(CrmConfig), nameof(CrmConfig.AssignmentConfig), "'{}'::jsonb")]
[InlineData(typeof(CrmWebhookQueueItem), nameof(CrmWebhookQueueItem.Payload), "'{}'::jsonb")]
[InlineData(typeof(CrmWebhookLog), nameof(CrmWebhookLog.RequestPayload), "'{}'::jsonb")]
[InlineData(typeof(TenantCommissionSetting), nameof(TenantCommissionSetting.Config), "'{}'::jsonb")]
[InlineData(typeof(CommissionSettlement), nameof(CommissionSettlement.Metadata), "'{}'::jsonb")]
[InlineData(typeof(CommissionSettlementItem), nameof(CommissionSettlementItem.Metadata), "'{}'::jsonb")]
public void Growth_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 Growth_tables_have_expected_unique_indexes_and_decimal_precision()
{
using var context = new TikuDbContext(Options);
AssertHasUniqueIndex<ReferralCode>(nameof(ReferralCode.TenantId), nameof(ReferralCode.Code));
AssertHasUniqueIndex<ReferralLead>(nameof(ReferralLead.TenantId), nameof(ReferralLead.StudentUserId));
AssertHasUniqueIndex<CrmConfig>(nameof(CrmConfig.TenantId));
AssertHasUniqueIndex<CrmWebhookQueueItem>(nameof(CrmWebhookQueueItem.TenantId), nameof(CrmWebhookQueueItem.IdempotencyKey));
AssertHasUniqueIndex<CommissionSettlement>(
nameof(CommissionSettlement.TenantId),
nameof(CommissionSettlement.SettlementNo));
AssertHasUniqueIndex<CommissionSettlementItem>(
nameof(CommissionSettlementItem.TenantId),
nameof(CommissionSettlementItem.SourceType),
nameof(CommissionSettlementItem.SourceId));
AssertPrecision<TenantCommissionSetting>(nameof(TenantCommissionSetting.DefaultRate), 6, 4);
AssertPrecision<CommissionSettlement>(nameof(CommissionSettlement.DefaultRate), 6, 4);
AssertPrecision<CommissionSettlementItem>(nameof(CommissionSettlementItem.CommissionRate), 6, 4);
void AssertHasUniqueIndex<TEntity>(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));
}
void AssertPrecision<TEntity>(string propertyName, int precision, int scale)
{
var property = context.Model.FindEntityType(typeof(TEntity))!
.FindProperty(propertyName)!;
Assert.Equal(precision, property.GetPrecision());
Assert.Equal(scale, property.GetScale());
}
}
[Theory]
[InlineData(typeof(AuditLog), nameof(AuditLog.Details), "'{}'::jsonb")]
[InlineData(typeof(UserNotification), nameof(UserNotification.Metadata), "'{}'::jsonb")]
[InlineData(typeof(Badge), nameof(Badge.ConditionExtra), "'{}'::jsonb")]
[InlineData(typeof(TenantContentNotification), nameof(TenantContentNotification.Metadata), "'{}'::jsonb")]
[InlineData(typeof(TenantThemeTemplate), nameof(TenantThemeTemplate.Theme), "'{}'::jsonb")]
[InlineData(typeof(TenantThemeTemplate), nameof(TenantThemeTemplate.PublicAssets), "'{}'::jsonb")]
[InlineData(typeof(TenantThemeConfig), nameof(TenantThemeConfig.ActiveTheme), "'{}'::jsonb")]
[InlineData(typeof(TenantThemeConfig), nameof(TenantThemeConfig.ActivePublicAssets), "'{}'::jsonb")]
[InlineData(typeof(TenantThemeConfig), nameof(TenantThemeConfig.DraftTheme), "'{}'::jsonb")]
[InlineData(typeof(TenantThemeConfig), nameof(TenantThemeConfig.DraftPublicAssets), "'{}'::jsonb")]
public void Operations_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 Operations_tables_have_expected_unique_indexes_and_condition_precision()
{
using var context = new TikuDbContext(Options);
AssertHasUniqueIndex<Banner>(nameof(Banner.TenantId), nameof(Banner.LegacyId));
AssertHasUniqueIndex<UserNotification>(
nameof(UserNotification.TenantId),
nameof(UserNotification.UserId),
nameof(UserNotification.NotificationType),
nameof(UserNotification.DedupeKey));
AssertHasUniqueIndex<Badge>(nameof(Badge.TenantId), nameof(Badge.LegacyId));
AssertHasUniqueIndex<UserBadge>(
nameof(UserBadge.TenantId),
nameof(UserBadge.UserId),
nameof(UserBadge.BadgeId));
AssertHasUniqueIndex<TenantThemeTemplate>(nameof(TenantThemeTemplate.Code));
AssertHasUniqueIndex<TenantThemeConfig>(nameof(TenantThemeConfig.TenantId));
var conditionValue = context.Model.FindEntityType(typeof(Badge))!
.FindProperty(nameof(Badge.ConditionValue))!;
Assert.Equal(18, conditionValue.GetPrecision());
Assert.Equal(4, conditionValue.GetScale());
void AssertHasUniqueIndex<TEntity>(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));
}
}
}