using System; using Duplicati.Proprietary.DiskImage.General; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; namespace Duplicati.UnitTest.DiskImage.UnitTests; public partial class DiskImageUnitTests : BasicSetupHelper { /// /// Tests that a fully populated GeometryMetadata can be serialized to JSON and /// deserialized back, preserving all field values. /// [Test] public void Test_GeometryMetadata_RoundTrip_PreservesAllFields() { // Create a GeometryMetadata with all fields populated var original = new GeometryMetadata { Version = 1, Disk = new DiskGeometry { DevicePath = "/dev/sda", Size = 100 * MiB, SectorSize = 512, Sectors = (int)(100 * MiB / 512), TableType = PartitionTableType.GPT }, PartitionTable = new PartitionTableGeometry { Type = PartitionTableType.GPT, Size = 17408, // Typical GPT size SectorSize = 512, HasProtectiveMbr = true, HeaderSize = 92 }, Partitions = [ new PartitionGeometry { Number = 1, Type = PartitionType.Primary, StartOffset = 512 * 2048, Size = 40 * MiB, Name = "Test Partition 1", FilesystemType = FileSystemType.FAT32, VolumeGuid = Guid.Parse("12345678-1234-1234-1234-123456789abc"), TableType = PartitionTableType.GPT }, new PartitionGeometry { Number = 2, Type = PartitionType.Primary, StartOffset = 512 * 2048 + 40 * MiB, Size = 40 * MiB, Name = "Test Partition 2", FilesystemType = FileSystemType.NTFS, VolumeGuid = Guid.Parse("abcdef12-3456-7890-abcd-ef1234567890"), TableType = PartitionTableType.GPT } ], Filesystems = [ new FilesystemGeometry { PartitionNumber = 1, Type = FileSystemType.FAT32, PartitionStartOffset = 512 * 2048, BlockSize = 4096, Metadata = "{\"label\":\"FAT32_VOL\",\"serial\":\"1234-5678\"}" }, new FilesystemGeometry { PartitionNumber = 2, Type = FileSystemType.NTFS, PartitionStartOffset = 512 * 2048 + 40 * MiB, BlockSize = 4096, Metadata = "{\"label\":\"NTFS_VOL\",\"guid\":\"{12345678-1234-1234-1234-123456789ABC}\"}" } ] }; // Serialize to JSON var json = original.ToJson(); // Verify JSON is not null or empty Assert.IsNotNull(json, "JSON output should not be null."); Assert.IsNotEmpty(json, "JSON output should not be empty."); // Deserialize from JSON var deserialized = GeometryMetadata.FromJson(json); // Verify deserialized object is not null Assert.IsNotNull(deserialized, "Deserialized object should not be null."); // Verify Version Assert.AreEqual(original.Version, deserialized!.Version, "Version should match."); // Verify Disk properties Assert.IsNotNull(deserialized.Disk, "Disk should not be null after deserialization."); Assert.AreEqual(original.Disk!.DevicePath, deserialized.Disk!.DevicePath, "Disk DevicePath should match."); Assert.AreEqual(original.Disk.Size, deserialized.Disk.Size, "Disk Size should match."); Assert.AreEqual(original.Disk.SectorSize, deserialized.Disk.SectorSize, "Disk SectorSize should match."); Assert.AreEqual(original.Disk.Sectors, deserialized.Disk.Sectors, "Disk Sectors should match."); Assert.AreEqual(original.Disk.TableType, deserialized.Disk.TableType, "Disk TableType should match."); // Verify PartitionTable properties Assert.IsNotNull(deserialized.PartitionTable, "PartitionTable should not be null after deserialization."); Assert.AreEqual(original.PartitionTable!.Type, deserialized.PartitionTable!.Type, "PartitionTable Type should match."); Assert.AreEqual(original.PartitionTable.Size, deserialized.PartitionTable.Size, "PartitionTable Size should match."); Assert.AreEqual(original.PartitionTable.SectorSize, deserialized.PartitionTable.SectorSize, "PartitionTable SectorSize should match."); Assert.AreEqual(original.PartitionTable.HasProtectiveMbr, deserialized.PartitionTable.HasProtectiveMbr, "PartitionTable HasProtectiveMbr should match."); Assert.AreEqual(original.PartitionTable.HeaderSize, deserialized.PartitionTable.HeaderSize, "PartitionTable HeaderSize should match."); // Verify Partitions Assert.IsNotNull(deserialized.Partitions, "Partitions should not be null after deserialization."); Assert.AreEqual(original.Partitions!.Count, deserialized.Partitions!.Count, "Partition count should match."); for (int i = 0; i < original.Partitions.Count; i++) { var originalPart = original.Partitions[i]; var deserializedPart = deserialized.Partitions[i]; Assert.AreEqual(originalPart.Number, deserializedPart.Number, $"Partition {i + 1} Number should match."); Assert.AreEqual(originalPart.Type, deserializedPart.Type, $"Partition {i + 1} Type should match."); Assert.AreEqual(originalPart.StartOffset, deserializedPart.StartOffset, $"Partition {i + 1} StartOffset should match."); Assert.AreEqual(originalPart.Size, deserializedPart.Size, $"Partition {i + 1} Size should match."); Assert.AreEqual(originalPart.Name, deserializedPart.Name, $"Partition {i + 1} Name should match."); Assert.AreEqual(originalPart.FilesystemType, deserializedPart.FilesystemType, $"Partition {i + 1} FilesystemType should match."); Assert.AreEqual(originalPart.VolumeGuid, deserializedPart.VolumeGuid, $"Partition {i + 1} VolumeGuid should match."); Assert.AreEqual(originalPart.TableType, deserializedPart.TableType, $"Partition {i + 1} TableType should match."); } // Verify Filesystems Assert.IsNotNull(deserialized.Filesystems, "Filesystems should not be null after deserialization."); Assert.AreEqual(original.Filesystems!.Count, deserialized.Filesystems!.Count, "Filesystem count should match."); for (int i = 0; i < original.Filesystems.Count; i++) { var originalFs = original.Filesystems[i]; var deserializedFs = deserialized.Filesystems[i]; Assert.AreEqual(originalFs.PartitionNumber, deserializedFs.PartitionNumber, $"Filesystem {i + 1} PartitionNumber should match."); Assert.AreEqual(originalFs.Type, deserializedFs.Type, $"Filesystem {i + 1} Type should match."); Assert.AreEqual(originalFs.PartitionStartOffset, deserializedFs.PartitionStartOffset, $"Filesystem {i + 1} PartitionStartOffset should match."); Assert.AreEqual(originalFs.BlockSize, deserializedFs.BlockSize, $"Filesystem {i + 1} BlockSize should match."); Assert.AreEqual(originalFs.Metadata, deserializedFs.Metadata, $"Filesystem {i + 1} Metadata should match."); } } /// /// Tests that a GeometryMetadata with null optional fields can be serialized /// and deserialized correctly. /// [Test] public void Test_GeometryMetadata_RoundTrip_NullOptionalFields_PreservesStructure() { // Create a GeometryMetadata with minimal/null optional fields var original = new GeometryMetadata { Version = 1, Disk = new DiskGeometry { DevicePath = null, // Null optional field Size = 50 * MiB, SectorSize = 512, Sectors = (int)(50 * MiB / 512), TableType = PartitionTableType.MBR }, PartitionTable = new PartitionTableGeometry { Type = PartitionTableType.MBR, Size = 512, SectorSize = 512, HasProtectiveMbr = false, HeaderSize = 0, MbrSize = 512 }, Partitions = [ new PartitionGeometry { Number = 1, Type = PartitionType.Primary, StartOffset = 512 * 2048, Size = 40 * MiB, Name = null, // Null optional field FilesystemType = FileSystemType.Unknown, VolumeGuid = null, // Null optional field TableType = PartitionTableType.MBR } ], Filesystems = null // Null optional collection }; // Serialize to JSON var json = original.ToJson(); // Verify JSON is not null or empty Assert.IsNotNull(json, "JSON output should not be null."); Assert.IsNotEmpty(json, "JSON output should not be empty."); // Deserialize from JSON var deserialized = GeometryMetadata.FromJson(json); // Verify deserialized object is not null Assert.IsNotNull(deserialized, "Deserialized object should not be null."); // Verify null fields remain null Assert.IsNull(deserialized!.Disk!.DevicePath, "DevicePath should be null after deserialization."); Assert.IsNull(deserialized.Filesystems, "Filesystems should be null after deserialization."); // Verify partition with null fields Assert.IsNotNull(deserialized.Partitions, "Partitions should not be null."); Assert.AreEqual(1, deserialized.Partitions!.Count, "Should have 1 partition."); Assert.IsNull(deserialized.Partitions[0].Name, "Partition Name should be null."); Assert.IsNull(deserialized.Partitions[0].VolumeGuid, "Partition VolumeGuid should be null."); // Verify other fields are preserved Assert.AreEqual(original.Disk.Size, deserialized.Disk.Size, "Disk Size should match."); Assert.AreEqual(original.Disk.SectorSize, deserialized.Disk.SectorSize, "Disk SectorSize should match."); Assert.AreEqual(original.PartitionTable!.Type, deserialized.PartitionTable!.Type, "PartitionTable Type should match."); } /// /// Tests that an empty GeometryMetadata (with all null/empty fields) can be /// serialized and deserialized correctly. /// [Test] public void Test_GeometryMetadata_RoundTrip_EmptyMetadata_HandlesGracefully() { // Create a GeometryMetadata with mostly empty/null fields var original = new GeometryMetadata { Version = 1, Disk = null, PartitionTable = null, Partitions = null, Filesystems = null }; // Serialize to JSON var json = original.ToJson(); // Verify JSON is not null or empty Assert.IsNotNull(json, "JSON output should not be null."); Assert.IsNotEmpty(json, "JSON output should not be empty."); // Deserialize from JSON var deserialized = GeometryMetadata.FromJson(json); // Verify deserialized object is not null Assert.IsNotNull(deserialized, "Deserialized object should not be null."); // Verify fields are null Assert.IsNull(deserialized!.Disk, "Disk should be null."); Assert.IsNull(deserialized.PartitionTable, "PartitionTable should be null."); Assert.IsNull(deserialized.Partitions, "Partitions should be null."); Assert.IsNull(deserialized.Filesystems, "Filesystems should be null."); Assert.AreEqual(1, deserialized.Version, "Version should be preserved."); } /// /// Tests that a GeometryMetadata with multiple partitions and filesystems of /// different types can be serialized and deserialized correctly. /// [Test] public void Test_GeometryMetadata_RoundTrip_MultiplePartitionsAndFilesystems_PreservesAllData() { // Create a GeometryMetadata with multiple partitions and filesystems var original = new GeometryMetadata { Version = 1, Disk = new DiskGeometry { DevicePath = "/dev/nvme0n1", Size = 500 * MiB, SectorSize = 4096, // 4K sectors Sectors = (int)(500 * MiB / 4096), TableType = PartitionTableType.GPT }, PartitionTable = new PartitionTableGeometry { Type = PartitionTableType.GPT, Size = 32768, SectorSize = 4096, HasProtectiveMbr = true, HeaderSize = 92 }, Partitions = [ new PartitionGeometry { Number = 1, Type = PartitionType.Primary, StartOffset = 4096 * 6, // Start at sector 6 (after GPT headers) Size = 100 * MiB, Name = "EFI System Partition", FilesystemType = FileSystemType.FAT32, VolumeGuid = Guid.NewGuid(), TableType = PartitionTableType.GPT }, new PartitionGeometry { Number = 2, Type = PartitionType.Primary, StartOffset = 4096 * 6 + 100 * MiB, Size = 150 * MiB, Name = "Windows OS", FilesystemType = FileSystemType.NTFS, VolumeGuid = Guid.NewGuid(), TableType = PartitionTableType.GPT }, new PartitionGeometry { Number = 3, Type = PartitionType.Primary, StartOffset = 4096 * 6 + 250 * MiB, Size = 200 * MiB, Name = "Linux Root", FilesystemType = FileSystemType.Ext4, VolumeGuid = Guid.NewGuid(), TableType = PartitionTableType.GPT } ], Filesystems = [ new FilesystemGeometry { PartitionNumber = 1, Type = FileSystemType.FAT32, PartitionStartOffset = 4096 * 6, BlockSize = 4096, Metadata = "{\"label\":\"EFI\"}" }, new FilesystemGeometry { PartitionNumber = 2, Type = FileSystemType.NTFS, PartitionStartOffset = 4096 * 6 + 100 * MiB, BlockSize = 4096, Metadata = "{\"label\":\"Windows\"}" }, new FilesystemGeometry { PartitionNumber = 3, Type = FileSystemType.Ext4, PartitionStartOffset = 4096 * 6 + 250 * MiB, BlockSize = 4096, Metadata = "{\"label\":\"/\"}" } ] }; // Serialize to JSON var json = original.ToJson(); // Verify JSON is not null or empty Assert.IsNotNull(json, "JSON output should not be null."); Assert.IsNotEmpty(json, "JSON output should not be empty."); // Deserialize from JSON var deserialized = GeometryMetadata.FromJson(json); // Verify deserialized object is not null Assert.IsNotNull(deserialized, "Deserialized object should not be null."); // Verify counts Assert.AreEqual(3, deserialized!.Partitions!.Count, "Should have 3 partitions."); Assert.AreEqual(3, deserialized.Filesystems!.Count, "Should have 3 filesystems."); // Verify each partition's filesystem type Assert.AreEqual(FileSystemType.FAT32, deserialized.Partitions[0].FilesystemType, "Partition 1 should be FAT32."); Assert.AreEqual(FileSystemType.NTFS, deserialized.Partitions[1].FilesystemType, "Partition 2 should be NTFS."); Assert.AreEqual(FileSystemType.Ext4, deserialized.Partitions[2].FilesystemType, "Partition 3 should be Ext4."); // Verify each filesystem's type Assert.AreEqual(FileSystemType.FAT32, deserialized.Filesystems[0].Type, "Filesystem 1 should be FAT32."); Assert.AreEqual(FileSystemType.NTFS, deserialized.Filesystems[1].Type, "Filesystem 2 should be NTFS."); Assert.AreEqual(FileSystemType.Ext4, deserialized.Filesystems[2].Type, "Filesystem 3 should be Ext4."); // Verify 4K sector size is preserved Assert.AreEqual(4096, deserialized.Disk!.SectorSize, "Disk SectorSize should be 4096."); Assert.AreEqual(4096, deserialized.PartitionTable!.SectorSize, "PartitionTable SectorSize should be 4096."); } /// /// Tests that serializing an MBR-based GeometryMetadata produces valid JSON /// that can be correctly deserialized. /// [Test] public void Test_GeometryMetadata_RoundTrip_MBRType_PreservesMBRData() { var original = new GeometryMetadata { Version = 1, Disk = new DiskGeometry { DevicePath = "/dev/sdb", Size = 200 * MiB, SectorSize = 512, Sectors = (int)(200 * MiB / 512), TableType = PartitionTableType.MBR }, PartitionTable = new PartitionTableGeometry { Type = PartitionTableType.MBR, Size = 512, SectorSize = 512, HasProtectiveMbr = false, MbrSize = 512 }, Partitions = [ new PartitionGeometry { Number = 1, Type = PartitionType.Primary, StartOffset = 512 * 2048, Size = 100 * MiB, Name = "Primary Partition", FilesystemType = FileSystemType.FAT32, TableType = PartitionTableType.MBR }, new PartitionGeometry { Number = 2, Type = PartitionType.Primary, StartOffset = 512 * 2048 + 100 * MiB, Size = 90 * MiB, Name = "Secondary Partition", FilesystemType = FileSystemType.NTFS, TableType = PartitionTableType.MBR } ], Filesystems = [ new FilesystemGeometry { PartitionNumber = 1, Type = FileSystemType.FAT32, PartitionStartOffset = 512 * 2048, BlockSize = 4096, Metadata = null } ] }; // Serialize to JSON var json = original.ToJson(); // Deserialize from JSON var deserialized = GeometryMetadata.FromJson(json); // Verify MBR-specific fields Assert.IsNotNull(deserialized, "Deserialized object should not be null."); Assert.AreEqual(PartitionTableType.MBR, deserialized!.Disk!.TableType, "Disk TableType should be MBR."); Assert.AreEqual(PartitionTableType.MBR, deserialized.PartitionTable!.Type, "PartitionTable Type should be MBR."); Assert.AreEqual(512, deserialized.PartitionTable.MbrSize, "PartitionTable MbrSize should be 512."); Assert.IsFalse(deserialized.PartitionTable.HasProtectiveMbr, "PartitionTable HasProtectiveMbr should be false."); // Verify all partitions have MBR table type foreach (var partition in deserialized.Partitions!) { Assert.AreEqual(PartitionTableType.MBR, partition.TableType, "All partitions should have MBR TableType."); } } /// /// Tests that deserializing invalid JSON throws an appropriate exception. /// [Test] public void Test_GeometryMetadata_FromJson_InvalidJson_ThrowsException() { // Test with invalid JSON - should throw JsonException var invalidJson = "{invalid json"; Assert.Throws(() => GeometryMetadata.FromJson(invalidJson)); } /// /// Tests that deserializing empty JSON object creates a minimal GeometryMetadata /// with default property values set by the class. /// [Test] public void Test_GeometryMetadata_FromJson_EmptyObject_CreatesDefaultInstance() { var emptyJson = "{}"; var result = GeometryMetadata.FromJson(emptyJson); Assert.IsNotNull(result, "Should create an instance from empty JSON object."); // Version has a default value of 1 in the class definition Assert.AreEqual(1, result!.Version, "Version should have default value (1) from class definition."); Assert.IsNull(result.Disk, "Disk should be null."); Assert.IsNull(result.PartitionTable, "PartitionTable should be null."); Assert.IsNull(result.Partitions, "Partitions should be null."); Assert.IsNull(result.Filesystems, "Filesystems should be null."); } }