using System.IO.Compression; using System.Security.Cryptography; using ScadaLink.Commons.Types.Transport; using ScadaLink.Transport.Encryption; using ScadaLink.Transport.Serialization; namespace ScadaLink.Transport.Tests.Serialization; public sealed class BundleSerializerTests { private const int TestIterations = 10_000; private static BundleContentDto SampleContent() => new( TemplateFolders: new[] { new TemplateFolderDto("Root", ParentName: null, SortOrder: 0) }, Templates: Array.Empty(), SharedScripts: new[] { new SharedScriptDto("util", "return 42;", ParameterDefinitions: null, ReturnDefinition: null) }, ExternalSystems: Array.Empty(), DatabaseConnections: Array.Empty(), NotificationLists: Array.Empty(), SmtpConfigs: Array.Empty(), ApiKeys: Array.Empty(), ApiMethods: Array.Empty()); private static BundleManifest BuildManifestFor(byte[] contentBytes, EncryptionMetadata? encryption = null) => new ManifestBuilder().Build( sourceEnvironment: "test-env", exportedBy: "tester", scadaLinkVersion: "1.0.0", encryption: encryption, summary: new BundleSummary(0, 1, 1, 0, 0, 0, 0, 0, 0), contents: Array.Empty(), contentBytes: contentBytes); [Fact] public void Pack_emits_manifest_and_content_json_when_no_passphrase() { var sut = new BundleSerializer(); var content = SampleContent(); var contentJson = sut.SerializeContentBytes(content); var manifest = BuildManifestFor(contentJson); using var stream = sut.Pack(content, manifest, passphrase: null, encryptor: null); using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true); Assert.NotNull(archive.GetEntry("manifest.json")); Assert.NotNull(archive.GetEntry("content.json")); Assert.Null(archive.GetEntry("content.enc")); } [Fact] public void Pack_emits_manifest_and_content_enc_when_passphrase_supplied() { var sut = new BundleSerializer(); var encryptor = new BundleSecretEncryptor(); var content = SampleContent(); var contentJson = sut.SerializeContentBytes(content); var (cipher, meta) = encryptor.Encrypt(contentJson, "pass", TestIterations); var manifest = BuildManifestFor(cipher, encryption: meta); using var stream = sut.Pack(content, manifest, passphrase: "pass", encryptor: encryptor); using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true); Assert.NotNull(archive.GetEntry("manifest.json")); Assert.Null(archive.GetEntry("content.json")); Assert.NotNull(archive.GetEntry("content.enc")); } [Fact] public void Roundtrip_through_temp_stream_recovers_identical_content() { var sut = new BundleSerializer(); var content = SampleContent(); var contentJson = sut.SerializeContentBytes(content); var manifest = BuildManifestFor(contentJson); using var stream = sut.Pack(content, manifest, passphrase: null, encryptor: null); stream.Position = 0; var readManifest = sut.ReadManifest(stream); stream.Position = 0; var contentBytes = sut.ReadContentBytes(stream, readManifest); var unpacked = sut.UnpackContent(contentBytes, readManifest, passphrase: null, encryptor: null); Assert.Equal(manifest.ContentHash, readManifest.ContentHash); Assert.Single(unpacked.TemplateFolders); Assert.Equal("Root", unpacked.TemplateFolders[0].Name); var shared = Assert.Single(unpacked.SharedScripts); Assert.Equal("util", shared.Name); Assert.Equal("return 42;", shared.Code); } [Fact] public void ReadManifest_throws_when_zip_missing_manifest_json() { var sut = new BundleSerializer(); using var ms = new MemoryStream(); using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) { var entry = archive.CreateEntry("garbage.txt"); using var es = entry.Open(); es.Write(new byte[] { 1, 2, 3 }); } ms.Position = 0; Assert.Throws(() => sut.ReadManifest(ms)); } [Fact] public void UnpackContent_throws_CryptographicException_on_wrong_passphrase() { var sut = new BundleSerializer(); var encryptor = new BundleSecretEncryptor(); var content = SampleContent(); var contentJson = sut.SerializeContentBytes(content); var (cipher, meta) = encryptor.Encrypt(contentJson, "right", TestIterations); var manifest = BuildManifestFor(cipher, encryption: meta); using var stream = sut.Pack(content, manifest, passphrase: "right", encryptor: encryptor); stream.Position = 0; var readManifest = sut.ReadManifest(stream); stream.Position = 0; var contentBytes = sut.ReadContentBytes(stream, readManifest); Assert.ThrowsAny(() => sut.UnpackContent(contentBytes, readManifest, "wrong", encryptor)); } }