using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FrameworkBLL.Promotion;
using FrameworkDAL.CustomCode.Promotion;
using FrameworkDAL.DTO.Promotion;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.Vault;
using Microsoft.Extensions.Configuration;
using Moq;
using Xunit;
namespace FrameworkTests;
///
/// Round-trip coverage for Metadata Promotion's Tier 1 package encryption — hybrid RSA+AES-GCM
/// (a random per-package AES-256 key does the bulk encryption, wrapped with the destination's
/// RSA public key, since RSA alone can't handle payloads larger than its key size). No live Vault
/// or database needed: IPromotionCounterpartyDAL/IVaultService are both mockable interfaces, and
/// this test generates its own real RSA key pair in-memory rather than using a fixture key.
///
public class PromotionPackageCryptoTests
{
private static LoginDTO Login() => new() { ClientId = -1, UserId = 7 };
private static (string publicPem, string privatePem) GenerateKeyPair()
{
using var rsa = RSA.Create(2048);
return (rsa.ExportRSAPublicKeyPem(), rsa.ExportPkcs8PrivateKeyPem());
}
private static PromotionPackageCrypto CryptoWith(
string destinationPublicPem, string ownPrivatePem)
{
var counterpartyDal = new Mock();
counterpartyDal.Setup(d => d.GetByEnvironmentCode("IMPSYS-PARTNERX", It.IsAny(), It.IsAny()))
.ReturnsAsync(new PromotionCounterpartyDTO
{
EnvironmentCode = "IMPSYS-PARTNERX",
PublicKeyPem = destinationPublicPem
});
var vault = new Mock();
vault.Setup(v => v.GetSecretAsync("promotion-private-key", It.IsAny()))
.ReturnsAsync(ownPrivatePem);
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new System.Collections.Generic.Dictionary
{
["Promotion:PrivateKeyVaultKey"] = "promotion-private-key"
}).Build();
return new PromotionPackageCrypto(counterpartyDal.Object, vault.Object, config);
}
[Fact]
public async Task EncryptThenDecrypt_RoundTrips_ToTheOriginalPlaintext()
{
var (publicPem, privatePem) = GenerateKeyPair();
var crypto = CryptoWith(publicPem, privatePem);
var plaintext = Encoding.UTF8.GetBytes(
"{\"EntityTypeCode\":\"MENU\",\"OriginEntityId\":42,\"PayloadJson\":\"{ some menu json }\"}");
var encrypted = await crypto.EncryptPackageAsync(plaintext, "IMPSYS-PARTNERX", Login(), CancellationToken.None);
Assert.NotEqual(plaintext, encrypted); // sanity: it's actually encrypted, not passed through
var decrypted = await crypto.DecryptPackageAsync(encrypted, CancellationToken.None);
Assert.Equal(plaintext, decrypted);
}
[Fact]
public async Task Decrypt_WithWrongPrivateKey_ThrowsRatherThanReturningGarbage()
{
var (publicPem, _) = GenerateKeyPair();
var (_, wrongPrivatePem) = GenerateKeyPair(); // an unrelated key pair
var crypto = CryptoWith(publicPem, wrongPrivatePem);
var plaintext = Encoding.UTF8.GetBytes("{\"EntityTypeCode\":\"MENU\"}");
var encrypted = await crypto.EncryptPackageAsync(plaintext, "IMPSYS-PARTNERX", Login(), CancellationToken.None);
// AES-GCM's authentication tag check fails first (RSA decrypt with the wrong key produces
// a garbage AES key, which then fails GCM's tag verification) — a real, loud failure, not
// a silent wrong-plaintext result.
await Assert.ThrowsAnyAsync(
() => crypto.DecryptPackageAsync(encrypted, CancellationToken.None));
}
[Fact]
public async Task Encrypt_UnknownCounterparty_ThrowsBeforeTouchingCrypto()
{
var counterpartyDal = new Mock();
counterpartyDal.Setup(d => d.GetByEnvironmentCode("UNKNOWN-ENV", It.IsAny(), It.IsAny()))
.ReturnsAsync((PromotionCounterpartyDTO?)null);
var crypto = new PromotionPackageCrypto(
counterpartyDal.Object, Mock.Of(), new ConfigurationBuilder().Build());
await Assert.ThrowsAsync(() =>
crypto.EncryptPackageAsync(new byte[] { 1, 2, 3 }, "UNKNOWN-ENV", Login(), CancellationToken.None));
}
}