feat(sms): complete SmsConfig bundle export/import wiring + GetSmsConfigurationByIdAsync (S10b)
This commit is contained in:
@@ -213,6 +213,18 @@ public class NotificationRepositoryTests : IDisposable
|
||||
Assert.Equal("smtp.example.test", loaded!.Host);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSmsConfiguration_AndGetById_RoundTrips()
|
||||
{
|
||||
var sms = new SmsConfiguration("ACbyid123", "+14155550111");
|
||||
await _repository.AddSmsConfigurationAsync(sms);
|
||||
await _repository.SaveChangesAsync();
|
||||
|
||||
var loaded = await _repository.GetSmsConfigurationByIdAsync(sms.Id);
|
||||
Assert.NotNull(loaded);
|
||||
Assert.Equal("ACbyid123", loaded!.AccountSid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteNotificationList_RemovesEntity()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -52,6 +53,16 @@ public sealed class BundleExporterTests : IDisposable
|
||||
services.AddDbContext<ScadaBridgeDbContext>(opts =>
|
||||
opts.UseInMemoryDatabase(dbName));
|
||||
|
||||
// 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>()));
|
||||
|
||||
// Repositories the resolver pulls from. M8 (B4): the resolver now injects
|
||||
// ISiteRepository to walk the site/data-connection/instance closure, so it
|
||||
// must be registered or the BuildServiceProvider-time graph resolution for
|
||||
@@ -201,6 +212,78 @@ public sealed class BundleExporterTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_emits_selected_sms_config_with_secret_and_summary_count()
|
||||
{
|
||||
// Arrange: seed one SMS provider config with a secret auth token.
|
||||
int smsId;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
var sms = new SmsConfiguration("AC_export_sid", "+15557654321")
|
||||
{
|
||||
AuthToken = "super-secret-token",
|
||||
MessagingServiceSid = "MG_svc",
|
||||
ApiBaseUrl = "https://api.twilio.example",
|
||||
};
|
||||
ctx.Set<SmsConfiguration>().Add(sms);
|
||||
await ctx.SaveChangesAsync();
|
||||
smsId = sms.Id;
|
||||
}
|
||||
|
||||
// Act: export selecting only the SMS config.
|
||||
Stream bundleStream;
|
||||
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 });
|
||||
|
||||
bundleStream = await exporter.ExportAsync(
|
||||
selection, user: "alice", sourceEnvironment: "dev",
|
||||
passphrase: null, cancellationToken: CancellationToken.None);
|
||||
}
|
||||
|
||||
byte[] bundleBytes;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
await bundleStream.CopyToAsync(ms);
|
||||
bundleBytes = ms.ToArray();
|
||||
}
|
||||
|
||||
// Assert: manifest summary counts the SMS config; content carries it with
|
||||
// the auth token preserved inside the SecretsBlock.
|
||||
var serializer = _provider.GetRequiredService<BundleSerializer>();
|
||||
BundleManifest manifest;
|
||||
using (var ms = new MemoryStream(bundleBytes, writable: false))
|
||||
{
|
||||
manifest = serializer.ReadManifest(ms);
|
||||
}
|
||||
Assert.Equal(1, manifest.Summary.SmsConfigs);
|
||||
|
||||
byte[] rawContent;
|
||||
using (var ms = new MemoryStream(bundleBytes, writable: false))
|
||||
{
|
||||
rawContent = serializer.ReadContentBytes(ms, manifest);
|
||||
}
|
||||
var content = serializer.UnpackContent(rawContent, manifest, passphrase: null, encryptor: null);
|
||||
Assert.Single(content.SmsConfigs);
|
||||
var dto = content.SmsConfigs[0];
|
||||
Assert.Equal("AC_export_sid", dto.AccountSid);
|
||||
Assert.Equal("+15557654321", dto.FromNumber);
|
||||
Assert.NotNull(dto.Secrets);
|
||||
Assert.True(dto.Secrets!.Values.TryGetValue("AuthToken", out var token));
|
||||
Assert.Equal("super-secret-token", token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_with_passphrase_produces_encrypted_bundle()
|
||||
{
|
||||
|
||||
+147
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -61,6 +62,16 @@ public sealed class BundleImporterApplyTests : 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>();
|
||||
@@ -768,6 +779,142 @@ public sealed class BundleImporterApplyTests : IDisposable
|
||||
Assert.Equal(1, result.Overwritten);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// S10b: SMS provider config bundle apply. Mirrors the SMTP apply path —
|
||||
// create a new config (Add) and overwrite an existing one (matched by the
|
||||
// natural key AccountSid), with the auth token decrypted from the
|
||||
// SecretsBlock. Hand-packs a BundleContentDto with SmsConfigs set so the
|
||||
// test is decoupled from the export selection (covered separately).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
private async Task<Guid> PackAndLoadSmsBundleAsync(params SmsConfigDto[] smsConfigs)
|
||||
{
|
||||
var content = new BundleContentDto(
|
||||
TemplateFolders: Array.Empty<TemplateFolderDto>(),
|
||||
Templates: Array.Empty<TemplateDto>(),
|
||||
SharedScripts: Array.Empty<SharedScriptDto>(),
|
||||
ExternalSystems: Array.Empty<ExternalSystemDto>(),
|
||||
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
||||
NotificationLists: Array.Empty<NotificationListDto>(),
|
||||
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
||||
ApiMethods: Array.Empty<ApiMethodDto>())
|
||||
{
|
||||
SmsConfigs = smsConfigs,
|
||||
};
|
||||
|
||||
await using var scope = _provider.CreateAsyncScope();
|
||||
var serializer = scope.ServiceProvider.GetRequiredService<BundleSerializer>();
|
||||
var manifestBuilder = scope.ServiceProvider.GetRequiredService<ManifestBuilder>();
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
|
||||
var contentBytes = serializer.SerializeContentBytes(content);
|
||||
var manifest = manifestBuilder.Build(
|
||||
sourceEnvironment: "dev",
|
||||
exportedBy: "alice",
|
||||
scadaBridgeVersion: "1.0.0",
|
||||
encryption: null,
|
||||
summary: new BundleSummary(0, 0, 0, 0, 0, 0, 0, 0, SmsConfigs: smsConfigs.Length),
|
||||
contents: Array.Empty<ManifestContentEntry>(),
|
||||
contentBytes: contentBytes);
|
||||
await using var packed = serializer.Pack(content, manifest, passphrase: null, encryptor: null);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await packed.CopyToAsync(ms);
|
||||
ms.Position = 0;
|
||||
var session = await importer.LoadAsync(ms, passphrase: null);
|
||||
return session.SessionId;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_creates_new_sms_config_with_decrypted_secret()
|
||||
{
|
||||
var dto = new SmsConfigDto(
|
||||
AccountSid: "AC_new_sid",
|
||||
FromNumber: "+15551112222",
|
||||
MessagingServiceSid: "MG_svc",
|
||||
ApiBaseUrl: "https://api.twilio.example",
|
||||
ConnectionTimeoutSeconds: 30,
|
||||
MaxRetries: 10,
|
||||
RetryDelay: TimeSpan.FromMinutes(1),
|
||||
Secrets: new SecretsBlock(new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["AuthToken"] = "fresh-token",
|
||||
}));
|
||||
var sessionId = await PackAndLoadSmsBundleAsync(dto);
|
||||
|
||||
ImportResult result;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
result = await importer.ApplyAsync(sessionId,
|
||||
new List<ImportResolution> { new("SmsConfiguration", "AC_new_sid", ResolutionAction.Add, null) },
|
||||
user: "bob");
|
||||
}
|
||||
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
var saved = await ctx.Set<Commons.Entities.Notifications.SmsConfiguration>()
|
||||
.SingleAsync(s => s.AccountSid == "AC_new_sid");
|
||||
Assert.Equal("+15551112222", saved.FromNumber);
|
||||
Assert.Equal("MG_svc", saved.MessagingServiceSid);
|
||||
Assert.Equal("fresh-token", saved.AuthToken);
|
||||
}
|
||||
Assert.Equal(1, result.Added);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_overwrites_existing_sms_config_matched_by_account_sid()
|
||||
{
|
||||
// Seed an existing config with the SAME AccountSid but stale field + secret.
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
ctx.Set<Commons.Entities.Notifications.SmsConfiguration>().Add(
|
||||
new Commons.Entities.Notifications.SmsConfiguration("AC_existing_sid", "+15550000000")
|
||||
{
|
||||
AuthToken = "old-token",
|
||||
ApiBaseUrl = "https://old.example",
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var dto = new SmsConfigDto(
|
||||
AccountSid: "AC_existing_sid",
|
||||
FromNumber: "+15559998888",
|
||||
MessagingServiceSid: null,
|
||||
ApiBaseUrl: "https://new.example",
|
||||
ConnectionTimeoutSeconds: 45,
|
||||
MaxRetries: 5,
|
||||
RetryDelay: TimeSpan.FromMinutes(2),
|
||||
Secrets: new SecretsBlock(new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["AuthToken"] = "new-token",
|
||||
}));
|
||||
var sessionId = await PackAndLoadSmsBundleAsync(dto);
|
||||
|
||||
ImportResult result;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
result = await importer.ApplyAsync(sessionId,
|
||||
new List<ImportResolution> { new("SmsConfiguration", "AC_existing_sid", ResolutionAction.Overwrite, null) },
|
||||
user: "bob");
|
||||
}
|
||||
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
// Exactly one row for this AccountSid — Overwrite updated in place, no dup.
|
||||
var saved = await ctx.Set<Commons.Entities.Notifications.SmsConfiguration>()
|
||||
.SingleAsync(s => s.AccountSid == "AC_existing_sid");
|
||||
Assert.Equal("+15559998888", saved.FromNumber);
|
||||
Assert.Equal("https://new.example", saved.ApiBaseUrl);
|
||||
Assert.Equal("new-token", saved.AuthToken);
|
||||
}
|
||||
Assert.Equal(1, result.Overwritten);
|
||||
Assert.Equal(0, result.Added);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Re-arch C4 backward-compat: a LEGACY (pre-C4) bundle still carries an
|
||||
// ApiKeys section. The importer must ignore those keys gracefully — it must
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
@@ -64,6 +65,17 @@ public sealed class DependencyResolverTests
|
||||
SiteIds: siteIds ?? Array.Empty<int>(),
|
||||
InstanceIds: instanceIds);
|
||||
|
||||
private static ExportSelection SelectSmsConfigs(params int[] ids) => new(
|
||||
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: true,
|
||||
SmsConfigurationIds: ids);
|
||||
|
||||
private void StubTemplate(Template t)
|
||||
{
|
||||
_templates.GetTemplateWithChildrenAsync(t.Id, Arg.Any<CancellationToken>()).Returns(t);
|
||||
@@ -194,6 +206,26 @@ public sealed class DependencyResolverTests
|
||||
Assert.Equal("Validator", result.SharedScripts[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolve_includes_selected_sms_configs()
|
||||
{
|
||||
// Mirrors the SMTP/by-id seed path: a selected SMS config id resolves via
|
||||
// GetSmsConfigurationByIdAsync into ResolvedExport.SmsConfigs, and a manifest
|
||||
// row (keyed by AccountSid) is emitted alongside it.
|
||||
var sms = new SmsConfiguration("AC_test_sid", "+15551230000") { Id = 7 };
|
||||
_notifications.GetSmsConfigurationByIdAsync(7, Arg.Any<CancellationToken>()).Returns(sms);
|
||||
StubAllSharedScripts();
|
||||
StubAllExternalSystems();
|
||||
StubAllFolders();
|
||||
|
||||
var result = await Sut().ResolveAsync(SelectSmsConfigs(7), CancellationToken.None);
|
||||
|
||||
Assert.Single(result.SmsConfigs);
|
||||
Assert.Equal("AC_test_sid", result.SmsConfigs[0].AccountSid);
|
||||
Assert.Contains(result.ContentManifest,
|
||||
e => e.Type == "SmsConfiguration" && e.Name == "AC_test_sid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolve_handles_diamond_dependency_without_duplication()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user