Files
scadalink-design/tests/ScadaLink.Transport.IntegrationTests/ConflictResolutionTests.cs

232 lines
9.6 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ScadaLink.Commons.Entities.Templates;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Interfaces.Transport;
using ScadaLink.Commons.Types.Transport;
using ScadaLink.ConfigurationDatabase;
using ScadaLink.ConfigurationDatabase.Repositories;
using ScadaLink.ConfigurationDatabase.Services;
using ScadaLink.Transport;
namespace ScadaLink.Transport.IntegrationTests;
/// <summary>
/// T26 — integration conflict-resolution tests. The unit-level Apply paths
/// in <c>BundleImporterApplyTests</c> exercise hand-built sessions; this
/// suite drives the full export→load→apply pipeline so the wire-level
/// session round-trip is part of the assertion.
/// </summary>
public sealed class ConflictResolutionTests : IDisposable
{
private readonly ServiceProvider _provider;
public ConflictResolutionTests()
{
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(
new ConfigurationBuilder().AddInMemoryCollection().Build());
var dbName = $"ConflictResolutionTests_{Guid.NewGuid()}";
services.AddDbContext<ScadaLinkDbContext>(opts => opts
.UseInMemoryDatabase(dbName)
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)));
services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
services.AddScoped<IExternalSystemRepository, ExternalSystemRepository>();
services.AddScoped<INotificationRepository, NotificationRepository>();
services.AddScoped<IInboundApiRepository, InboundApiRepository>();
services.AddScoped<IAuditCorrelationContext, AuditCorrelationContext>();
services.AddScoped<IAuditService, AuditService>();
services.AddTransport();
_provider = services.BuildServiceProvider();
}
public void Dispose() => _provider.Dispose();
/// <summary>
/// Exports the current set of templates+shared scripts to a freshly built
/// bundle and immediately loads it into a session. Returns the
/// <see cref="BundleSession.SessionId"/> the caller can hand to ApplyAsync.
/// </summary>
private async Task<Guid> ExportAndLoadAsync()
{
byte[] bundleBytes;
await using (var scope = _provider.CreateAsyncScope())
{
var exporter = scope.ServiceProvider.GetRequiredService<IBundleExporter>();
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
var templateIds = await ctx.Templates.Select(t => t.Id).ToListAsync();
var sharedScriptIds = await ctx.SharedScripts.Select(s => s.Id).ToListAsync();
var selection = new ExportSelection(
TemplateIds: templateIds,
SharedScriptIds: sharedScriptIds,
ExternalSystemIds: Array.Empty<int>(),
DatabaseConnectionIds: Array.Empty<int>(),
NotificationListIds: Array.Empty<int>(),
SmtpConfigurationIds: Array.Empty<int>(),
ApiKeyIds: Array.Empty<int>(),
ApiMethodIds: Array.Empty<int>(),
IncludeDependencies: false);
var stream = await exporter.ExportAsync(selection,
user: "alice", sourceEnvironment: "dev",
passphrase: null, cancellationToken: CancellationToken.None);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
bundleBytes = ms.ToArray();
}
await using var loadScope = _provider.CreateAsyncScope();
var importer = loadScope.ServiceProvider.GetRequiredService<IBundleImporter>();
using var input = new MemoryStream(bundleBytes, writable: false);
var session = await importer.LoadAsync(input, passphrase: null);
return session.SessionId;
}
[Fact]
public async Task Overwrite_replaces_existing_template_description()
{
// Arrange: seed Pump with Description="new", export it (the bundle
// therefore carries "new"), then mutate the target's Pump to "old".
// Apply with Overwrite must restore "new".
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
ctx.Templates.Add(new Template("Pump") { Description = "new" });
await ctx.SaveChangesAsync();
}
var sessionId = await ExportAndLoadAsync();
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
var existing = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
existing.Description = "old";
await ctx.SaveChangesAsync();
}
// Act
ImportResult result;
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
result = await importer.ApplyAsync(sessionId,
new List<ImportResolution>
{
new("Template", "Pump", ResolutionAction.Overwrite, null),
},
user: "bob");
}
// Assert
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
var pump = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
Assert.Equal("new", pump.Description);
}
Assert.Equal(1, result.Overwritten);
}
[Fact]
public async Task Skip_leaves_existing_template_unchanged()
{
// Arrange: seed Pump with Description="keep", export, then mutate to
// "replace" so the bundle's body diverges. Skip must NOT touch the
// target row, and the summary must report Skipped=1.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
ctx.Templates.Add(new Template("Pump") { Description = "replace" });
await ctx.SaveChangesAsync();
}
var sessionId = await ExportAndLoadAsync();
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
var existing = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
existing.Description = "keep";
await ctx.SaveChangesAsync();
}
// Act
ImportResult result;
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
result = await importer.ApplyAsync(sessionId,
new List<ImportResolution>
{
new("Template", "Pump", ResolutionAction.Skip, null),
},
user: "bob");
}
// Assert
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
var pump = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
Assert.Equal("keep", pump.Description);
}
Assert.Equal(1, result.Skipped);
Assert.Equal(0, result.Added);
Assert.Equal(0, result.Overwritten);
}
[Fact]
public async Task Rename_creates_new_template_alongside_existing()
{
// Arrange: seed Pump, export, mutate description so the rename target
// is obviously the bundle's version. The original Pump must survive
// untouched and a second Pump.Imported template must materialise.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
ctx.Templates.Add(new Template("Pump") { Description = "from-bundle" });
await ctx.SaveChangesAsync();
}
var sessionId = await ExportAndLoadAsync();
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
var existing = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
existing.Description = "kept-original";
await ctx.SaveChangesAsync();
}
// Act
ImportResult result;
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
result = await importer.ApplyAsync(sessionId,
new List<ImportResolution>
{
new("Template", "Pump", ResolutionAction.Rename, "Pump.Imported"),
},
user: "bob");
}
// Assert
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaLinkDbContext>();
var original = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
Assert.Equal("kept-original", original.Description);
var renamed = await ctx.Templates.SingleAsync(t => t.Name == "Pump.Imported");
Assert.Equal("from-bundle", renamed.Description);
}
Assert.Equal(1, result.Renamed);
}
}