using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
///
/// Secret-elision projection tests (arch-review C3): connection-config secrets
/// (OPC UA passwords, external-system auth config, DB connection strings) must
/// never leave the management boundary via List/Get responses or audit afterState,
/// and sentinel/omitted round-trips through Update must preserve the stored secret.
///
public class SecretProjectionTests : TestKit, IDisposable
{
private readonly IAuditService _auditService;
private readonly ServiceCollection _services;
public SecretProjectionTests()
{
_auditService = Substitute.For();
_services = new ServiceCollection();
_services.AddScoped(_ => _auditService);
}
private IActorRef CreateActor()
{
var sp = _services.BuildServiceProvider();
return Sys.ActorOf(Props.Create(() => new ManagementActor(
sp, NullLogger.Instance)));
}
private static ManagementEnvelope Envelope(object command, params string[] roles) =>
new(new AuthenticatedUser("testuser", "Test User", roles, Array.Empty()),
command, Guid.NewGuid().ToString("N"));
void IDisposable.Dispose() => Shutdown();
// ========================================================================
// Task 7 — DataConnection config secret elision
// ========================================================================
private ISiteRepository AddSiteRepoWithConnection(DataConnection conn)
{
var siteRepo = Substitute.For();
siteRepo.GetDataConnectionByIdAsync(conn.Id, Arg.Any()).Returns(conn);
siteRepo.GetAllDataConnectionsAsync(Arg.Any())
.Returns(new List { conn });
_services.AddScoped(_ => siteRepo);
return siteRepo;
}
[Fact]
public void GetDataConnection_ResponseOmitsPasswords()
{
AddSiteRepoWithConnection(new DataConnection("c1", "OpcUa", 1)
{
Id = 1,
PrimaryConfiguration = """{"Endpoint":"opc.tcp://x","UserIdentity":{"Password":"opc-secret"}}""",
});
var actor = CreateActor();
actor.Tell(Envelope(new GetDataConnectionCommand(1), "Viewer"));
var resp = ExpectMsg(TimeSpan.FromSeconds(5));
Assert.DoesNotContain("opc-secret", resp.JsonData);
Assert.Contains("hasSecrets", resp.JsonData); // camelCase serializer
}
[Fact]
public void ListDataConnections_ResponseOmitsPasswords()
{
AddSiteRepoWithConnection(new DataConnection("c1", "OpcUa", 1)
{
Id = 1,
PrimaryConfiguration = """{"UserIdentity":{"Password":"opc-secret"}}""",
});
var actor = CreateActor();
actor.Tell(Envelope(new ListDataConnectionsCommand(null), "Viewer"));
var resp = ExpectMsg(TimeSpan.FromSeconds(5));
Assert.DoesNotContain("opc-secret", resp.JsonData);
Assert.Contains("hasSecrets", resp.JsonData);
}
[Fact]
public async Task UpdateDataConnection_SentinelConfig_PreservesStoredSecret()
{
var stored = new DataConnection("c1", "OpcUa", 1)
{
Id = 1,
PrimaryConfiguration = """{"Endpoint":"opc.tcp://old","UserIdentity":{"Password":"opc-secret"}}""",
};
var siteRepo = AddSiteRepoWithConnection(stored);
var actor = CreateActor();
var incoming = """{"Endpoint":"opc.tcp://new","UserIdentity":{"Password":"***REDACTED***"}}""";
actor.Tell(Envelope(new UpdateDataConnectionCommand(1, "c1", "OpcUa", incoming, null, 3), "Designer"));
ExpectMsg(TimeSpan.FromSeconds(5));
await siteRepo.Received(1).UpdateDataConnectionAsync(
Arg.Is(c => c.PrimaryConfiguration!.Contains("opc-secret")
&& !c.PrimaryConfiguration.Contains("***REDACTED***")),
Arg.Any());
}
[Fact]
public void UpdateDataConnection_AuditAfterState_OmitsPasswords()
{
var stored = new DataConnection("c1", "OpcUa", 1)
{
Id = 1,
PrimaryConfiguration = """{"Password":"opc-secret"}""",
};
AddSiteRepoWithConnection(stored);
var actor = CreateActor();
actor.Tell(Envelope(new UpdateDataConnectionCommand(1, "c1", "OpcUa", """{"Password":"newpw"}""", null, 3), "Designer"));
ExpectMsg(TimeSpan.FromSeconds(5));
_auditService.Received().LogAsync(Arg.Any(), "Update", "DataConnection",
Arg.Any(), Arg.Any(),
Arg.Is