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)); } }