feat(sms): complete SmsConfig bundle export/import wiring + GetSmsConfigurationByIdAsync (S10b)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -49,6 +50,16 @@ public sealed class RoundTripTests : IDisposable
|
||||
.UseInMemoryDatabase(dbName)
|
||||
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)));
|
||||
|
||||
// S10b: secret-bearing columns (SmsConfiguration.AuthToken, SmtpConfiguration
|
||||
// .Credentials, …) require the context's encrypting two-arg ctor with a Data
|
||||
// Protection key ring. Register an ephemeral provider and override the
|
||||
// AddDbContext registration to construct the encrypting context — mirrors the
|
||||
// production AddConfigurationDatabase wiring (and BundleImporterRollbackFailureTests).
|
||||
services.AddSingleton<IDataProtectionProvider>(new EphemeralDataProtectionProvider());
|
||||
services.AddScoped(sp => new ScadaBridgeDbContext(
|
||||
sp.GetRequiredService<DbContextOptions<ScadaBridgeDbContext>>(),
|
||||
sp.GetRequiredService<IDataProtectionProvider>()));
|
||||
|
||||
services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
|
||||
services.AddScoped<IExternalSystemRepository, ExternalSystemRepository>();
|
||||
services.AddScoped<INotificationRepository, NotificationRepository>();
|
||||
@@ -288,4 +299,103 @@ public sealed class RoundTripTests : IDisposable
|
||||
Assert.Equal(0, result.Skipped);
|
||||
Assert.NotEqual(Guid.Empty, result.BundleImportId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Export_then_import_restores_sms_config_with_secret_intact()
|
||||
{
|
||||
// S10b: full export→wipe→import round-trip for an SMS provider config,
|
||||
// through the real pipeline (export selection → resolver → exporter →
|
||||
// importer apply). Confirms the config + its secret survive the cycle.
|
||||
|
||||
// ---- 1. Seed one SMS config with an auth token. ----
|
||||
int smsId;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
var sms = new SmsConfiguration("AC_roundtrip_sid", "+15551239999")
|
||||
{
|
||||
AuthToken = "round-trip-token",
|
||||
MessagingServiceSid = "MG_rt",
|
||||
ApiBaseUrl = "https://api.example",
|
||||
};
|
||||
ctx.Set<SmsConfiguration>().Add(sms);
|
||||
await ctx.SaveChangesAsync();
|
||||
smsId = sms.Id;
|
||||
}
|
||||
|
||||
// ---- 2. Export selecting only the SMS config. ----
|
||||
byte[] bundleBytes;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var exporter = scope.ServiceProvider.GetRequiredService<IBundleExporter>();
|
||||
var selection = new ExportSelection(
|
||||
TemplateIds: Array.Empty<int>(),
|
||||
SharedScriptIds: Array.Empty<int>(),
|
||||
ExternalSystemIds: Array.Empty<int>(),
|
||||
DatabaseConnectionIds: Array.Empty<int>(),
|
||||
NotificationListIds: Array.Empty<int>(),
|
||||
SmtpConfigurationIds: Array.Empty<int>(),
|
||||
ApiMethodIds: Array.Empty<int>(),
|
||||
IncludeDependencies: false,
|
||||
SmsConfigurationIds: new[] { smsId });
|
||||
|
||||
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();
|
||||
}
|
||||
Assert.NotEmpty(bundleBytes);
|
||||
|
||||
// ---- 3. Wipe the source config so the import exercises the Add path. ----
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
ctx.Set<SmsConfiguration>().RemoveRange(ctx.Set<SmsConfiguration>());
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// ---- 4. Load → preview → apply (every item gets Add). ----
|
||||
Guid sessionId;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
using var ms = new MemoryStream(bundleBytes, writable: false);
|
||||
var session = await importer.LoadAsync(ms, passphrase: null);
|
||||
sessionId = session.SessionId;
|
||||
}
|
||||
|
||||
ImportPreview preview;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
preview = await importer.PreviewAsync(sessionId);
|
||||
}
|
||||
Assert.Contains(preview.Items,
|
||||
i => i.EntityType == "SmsConfiguration" && i.Name == "AC_roundtrip_sid" && i.Kind == ConflictKind.New);
|
||||
|
||||
var resolutions = preview.Items
|
||||
.Select(it => new ImportResolution(it.EntityType, it.Name, ResolutionAction.Add, null))
|
||||
.ToList();
|
||||
|
||||
ImportResult result;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
result = await importer.ApplyAsync(sessionId, resolutions, user: "bob");
|
||||
}
|
||||
|
||||
// ---- 5. The config is back with its secret. ----
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
var restored = await ctx.Set<SmsConfiguration>()
|
||||
.SingleAsync(s => s.AccountSid == "AC_roundtrip_sid");
|
||||
Assert.Equal("+15551239999", restored.FromNumber);
|
||||
Assert.Equal("MG_rt", restored.MessagingServiceSid);
|
||||
Assert.Equal("round-trip-token", restored.AuthToken);
|
||||
}
|
||||
Assert.Equal(1, result.Added);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user