224 lines
8.0 KiB
C#
224 lines
8.0 KiB
C#
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
using Npgsql;
|
|
using Workflow.Domain.Enums;
|
|
using Workflow.Infrastructure.Persistence;
|
|
using Xunit;
|
|
|
|
namespace BuildFlow.Workflow.Tests.Reliability;
|
|
|
|
/// <summary>
|
|
/// 工作流可靠性迁移的空库安装与旧结构升级测试。
|
|
/// </summary>
|
|
public class WorkflowJobMigrationTests
|
|
{
|
|
private const string PreviousMigration =
|
|
"20260719041812_FixWorkflowTaskAttachmentsColumn";
|
|
|
|
private const string DefaultConnection =
|
|
"Host=127.0.0.1;Port=55432;Username=buildflow;Password=buildflow;Database=postgres";
|
|
|
|
[Fact]
|
|
public async Task EmptyDatabaseMigratesToWorkflowJobReliabilitySchema()
|
|
{
|
|
var databaseName = $"workflow_migration_empty_{Guid.NewGuid():N}";
|
|
var connectionString = await CreateDatabaseAsync(databaseName);
|
|
try
|
|
{
|
|
await using var db = CreateDbContext(connectionString);
|
|
await db.Database.MigrateAsync();
|
|
|
|
await using var connection = new NpgsqlConnection(connectionString);
|
|
await connection.OpenAsync();
|
|
(await TableExistsAsync(connection, "b_workflow_timer_jobs")).Should().BeTrue();
|
|
var columns = await ReadColumnNamesAsync(connection, "b_import_jobs");
|
|
columns.Should().Contain(
|
|
"lease_owner",
|
|
"lease_expires_at",
|
|
"attempt_count",
|
|
"last_heartbeat_at");
|
|
}
|
|
finally
|
|
{
|
|
await DropDatabaseAsync(databaseName);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExistingImportJobSurvivesReliabilityMigration()
|
|
{
|
|
var databaseName = $"workflow_migration_upgrade_{Guid.NewGuid():N}";
|
|
var connectionString = await CreateDatabaseAsync(databaseName);
|
|
var jobId = Guid.NewGuid();
|
|
var formDefinitionId = Guid.NewGuid();
|
|
const string filePath = "/tmp/existing-import.xlsx";
|
|
try
|
|
{
|
|
await using var db = CreateDbContext(connectionString);
|
|
var migrator = db.GetService<IMigrator>();
|
|
await migrator.MigrateAsync(PreviousMigration);
|
|
|
|
var now = DateTime.UtcNow;
|
|
await db.Database.ExecuteSqlInterpolatedAsync($"""
|
|
INSERT INTO b_import_jobs (
|
|
id,
|
|
form_definition_id,
|
|
status,
|
|
file_url,
|
|
total_rows,
|
|
success_rows,
|
|
failed_rows,
|
|
created_by,
|
|
created_at,
|
|
updated_by,
|
|
updated_at,
|
|
is_deleted,
|
|
operator_ip)
|
|
VALUES (
|
|
{jobId},
|
|
{formDefinitionId},
|
|
{(int)BuildFlowImportJobStatus.Pending},
|
|
{filePath},
|
|
{0},
|
|
{0},
|
|
{0},
|
|
{"migration-test"},
|
|
{now},
|
|
{"migration-test"},
|
|
{now},
|
|
{false},
|
|
{"127.0.0.1"})
|
|
""");
|
|
|
|
await db.Database.MigrateAsync();
|
|
|
|
await using var connection = new NpgsqlConnection(connectionString);
|
|
await connection.OpenAsync();
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = """
|
|
SELECT id,
|
|
status,
|
|
file_url,
|
|
attempt_count,
|
|
lease_owner,
|
|
lease_expires_at,
|
|
last_heartbeat_at
|
|
FROM b_import_jobs
|
|
WHERE id = @job_id
|
|
""";
|
|
command.Parameters.AddWithValue("job_id", jobId);
|
|
await using var reader = await command.ExecuteReaderAsync();
|
|
(await reader.ReadAsync()).Should().BeTrue();
|
|
reader.GetGuid(0).Should().Be(jobId);
|
|
reader.GetInt32(1).Should().Be((int)BuildFlowImportJobStatus.Pending);
|
|
reader.GetString(2).Should().Be(filePath);
|
|
reader.GetInt32(3).Should().Be(0);
|
|
reader.IsDBNull(4).Should().BeTrue();
|
|
reader.IsDBNull(5).Should().BeTrue();
|
|
reader.IsDBNull(6).Should().BeTrue();
|
|
}
|
|
finally
|
|
{
|
|
await DropDatabaseAsync(databaseName);
|
|
}
|
|
}
|
|
|
|
private static WorkflowDbContext CreateDbContext(string connectionString)
|
|
{
|
|
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
|
|
.UseNpgsql(connectionString)
|
|
.UseSnakeCaseNamingConvention()
|
|
.Options;
|
|
return new WorkflowDbContext(options);
|
|
}
|
|
|
|
private static async Task<string> CreateDatabaseAsync(string databaseName)
|
|
{
|
|
var configured = Environment.GetEnvironmentVariable("BUILDFLOW_WORKFLOW_TEST_CONNECTION")
|
|
?? DefaultConnection;
|
|
var adminBuilder = new NpgsqlConnectionStringBuilder(configured)
|
|
{
|
|
Database = "postgres",
|
|
Pooling = false,
|
|
};
|
|
await using (var connection = new NpgsqlConnection(adminBuilder.ConnectionString))
|
|
{
|
|
await connection.OpenAsync();
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = $"CREATE DATABASE \"{databaseName}\"";
|
|
await command.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
return new NpgsqlConnectionStringBuilder(configured)
|
|
{
|
|
Database = databaseName,
|
|
Pooling = true,
|
|
}.ConnectionString;
|
|
}
|
|
|
|
private static async Task DropDatabaseAsync(string databaseName)
|
|
{
|
|
NpgsqlConnection.ClearAllPools();
|
|
var configured = Environment.GetEnvironmentVariable("BUILDFLOW_WORKFLOW_TEST_CONNECTION")
|
|
?? DefaultConnection;
|
|
var adminBuilder = new NpgsqlConnectionStringBuilder(configured)
|
|
{
|
|
Database = "postgres",
|
|
Pooling = false,
|
|
};
|
|
await using var connection = new NpgsqlConnection(adminBuilder.ConnectionString);
|
|
await connection.OpenAsync();
|
|
await using (var terminate = connection.CreateCommand())
|
|
{
|
|
terminate.CommandText =
|
|
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = @database AND pid <> pg_backend_pid()";
|
|
terminate.Parameters.AddWithValue("database", databaseName);
|
|
await terminate.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
await using var drop = connection.CreateCommand();
|
|
drop.CommandText = $"DROP DATABASE IF EXISTS \"{databaseName}\"";
|
|
await drop.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
private static async Task<bool> TableExistsAsync(
|
|
NpgsqlConnection connection,
|
|
string tableName)
|
|
{
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = """
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_name = @table_name)
|
|
""";
|
|
command.Parameters.AddWithValue("table_name", tableName);
|
|
return (bool)(await command.ExecuteScalarAsync())!;
|
|
}
|
|
|
|
private static async Task<List<string>> ReadColumnNamesAsync(
|
|
NpgsqlConnection connection,
|
|
string tableName)
|
|
{
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = """
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
AND table_name = @table_name
|
|
""";
|
|
command.Parameters.AddWithValue("table_name", tableName);
|
|
var columns = new List<string>();
|
|
await using var reader = await command.ExecuteReaderAsync();
|
|
while (await reader.ReadAsync())
|
|
{
|
|
columns.Add(reader.GetString(0));
|
|
}
|
|
|
|
return columns;
|
|
}
|
|
}
|