81654c0d33
Add normalized service and template catalogs, scoped administration, migration, validation, and regression coverage. Ref: IT-1601
107 lines
6.0 KiB
C#
107 lines
6.0 KiB
C#
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
using HrynCo.NotificationService.DAL.EF;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Npgsql;
|
|
|
|
namespace HrynCo.NotificationService.Web.IntegrationTests;
|
|
|
|
public sealed class EmailTemplateMigrationTests(TemplateDatabaseFixture fixture) : IClassFixture<TemplateDatabaseFixture>
|
|
{
|
|
[Fact]
|
|
public async Task EquivalentContracts_PreserveLanguageIdsContentAndSharedVariables_AndRerunSafely()
|
|
{
|
|
var connection = await fixture.CreateDatabaseAsync(legacy: true);
|
|
var en = await TemplateDatabaseFixture.InsertLegacyAsync(connection, "en", """[{"name":"Name","required":true},{"name":"Link","required":false}]""");
|
|
var uk = await TemplateDatabaseFixture.InsertLegacyAsync(connection, "UK", """[{"required":false,"name":"Link"},{"required":true,"name":"Name"}]""");
|
|
await MigrateAsync(connection);
|
|
await MigrateAsync(connection);
|
|
using var services = new ServiceCollection().AddNotificationDataAccess(connection).BuildServiceProvider();
|
|
using var scope = services.CreateScope();
|
|
var repository = scope.ServiceProvider.GetRequiredService<IEmailTemplateRepository>();
|
|
var templates = await repository.GetAllAsync("TestService", "Welcome");
|
|
Assert.Equal(2, templates.Count);
|
|
Assert.Contains(templates, t => t.Id == en && t.Subject == "Subject en" && t.HtmlBody == "<p>{{Name}}</p>");
|
|
Assert.Contains(templates, t => t.Id == uk && t.LanguageCode == "UK" && t.Subject == "Subject UK");
|
|
Assert.All(templates, t => Assert.Equal(2, t.Variables.Count));
|
|
await using var db = TemplateDatabaseFixture.CreateContext(connection);
|
|
await db.GetService<IMigrator>().MigrateAsync(TemplateDatabaseFixture.LegacyMigration);
|
|
await db.Database.MigrateAsync();
|
|
Assert.Equal(en, (await repository.GetAsync("TestService", "Welcome", "en"))!.Id);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("[{\"name\":\"Other\",\"required\":true}]", "Conflicting shared variables")]
|
|
[InlineData("{}", "Invalid template identity or variables array")]
|
|
[InlineData("[null]", "Invalid variables shape")]
|
|
[InlineData("[{\"name\":\"Name\",\"required\":\"yes\"}]", "Invalid variables shape")]
|
|
[InlineData("[{\"name\":\"Name\",\"required\":true},{\"name\":\"Name\",\"required\":true}]", "Duplicate variable names")]
|
|
public async Task InvalidLegacyData_StopsWithoutChangingSource_ThenCanRecover(string variables, string diagnostic)
|
|
{
|
|
var connection = await fixture.CreateDatabaseAsync(legacy: true);
|
|
await TemplateDatabaseFixture.InsertLegacyAsync(connection, "en", """[{"name":"Name","required":true}]""");
|
|
await TemplateDatabaseFixture.InsertLegacyAsync(connection, "uk", variables);
|
|
var error = await Assert.ThrowsAsync<PostgresException>(() => MigrateAsync(connection));
|
|
Assert.Contains(diagnostic, error.MessageText);
|
|
Assert.Contains("TestService", error.MessageText);
|
|
Assert.DoesNotContain("Hello", error.MessageText);
|
|
await TemplateDatabaseFixture.ExecuteAsync(connection, """
|
|
DO $$ BEGIN
|
|
IF to_regclass('public.services') IS NOT NULL OR (SELECT count(*) FROM email_templates) <> 2 THEN
|
|
RAISE EXCEPTION 'Migration partially changed source';
|
|
END IF;
|
|
END $$;
|
|
UPDATE email_templates SET variables = '[{"name":"Name","required":true}]'::jsonb;
|
|
""");
|
|
await MigrateAsync(connection);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExactLanguageCodes_RemainDistinctAfterMigration()
|
|
{
|
|
var connection = await fixture.CreateDatabaseAsync(legacy: true);
|
|
await TemplateDatabaseFixture.InsertLegacyAsync(connection, "ru", "[]");
|
|
await TemplateDatabaseFixture.InsertLegacyAsync(connection, "ru-RU", "[]");
|
|
await TemplateDatabaseFixture.InsertLegacyAsync(connection, "EN", "[]");
|
|
await TemplateDatabaseFixture.InsertLegacyAsync(connection, "en", "[]");
|
|
await MigrateAsync(connection);
|
|
await TemplateDatabaseFixture.ExecuteAsync(connection, """
|
|
DO $$ BEGIN
|
|
IF (SELECT count(*) FROM service_languages) <> 4
|
|
OR NOT EXISTS (SELECT 1 FROM service_languages WHERE code = 'ru-RU')
|
|
OR NOT EXISTS (SELECT 1 FROM service_languages WHERE code = 'EN') THEN
|
|
RAISE EXCEPTION 'Migration changed exact language codes';
|
|
END IF;
|
|
END $$;
|
|
""");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InterruptedConversion_RollsBackAndCanBeRetried()
|
|
{
|
|
var connectionString = await fixture.CreateDatabaseAsync(legacy: true);
|
|
await TemplateDatabaseFixture.InsertLegacyAsync(connectionString, "en", "[]");
|
|
await using var db = TemplateDatabaseFixture.CreateContext(connectionString);
|
|
var script = db.GetService<IMigrator>().GenerateScript(TemplateDatabaseFixture.LegacyMigration, options: MigrationsSqlGenerationOptions.NoTransactions);
|
|
script = script.Replace("ALTER TABLE email_templates DROP COLUMN", "PERFORM pg_sleep(2); ALTER TABLE email_templates DROP COLUMN");
|
|
await using (var connection = new NpgsqlConnection(connectionString))
|
|
{
|
|
await connection.OpenAsync();
|
|
await using var transaction = await connection.BeginTransactionAsync();
|
|
await using var command = new NpgsqlCommand("SET LOCAL statement_timeout = '500ms'; " + script, connection, transaction);
|
|
var error = await Assert.ThrowsAsync<PostgresException>(() => command.ExecuteNonQueryAsync());
|
|
Assert.Equal("57014", error.SqlState);
|
|
await transaction.RollbackAsync();
|
|
}
|
|
await MigrateAsync(connectionString);
|
|
}
|
|
|
|
private static async Task MigrateAsync(string connectionString)
|
|
{
|
|
await using var db = TemplateDatabaseFixture.CreateContext(connectionString);
|
|
await db.Database.MigrateAsync();
|
|
}
|
|
}
|