using NUnit.Framework; using ShrinkDataSaver; using ShrinkSDK.Inspection; using ShrinkNetwork; using Cysharp.Threading.Tasks; [NonParallelizable] public class SaveSnapshotTests { private string _directory = null!; [SetUp] public void Setup() { _directory = Path.Combine(Path.GetTempPath(), "ShrinkSaveTests", Guid.NewGuid().ToString("N")); ShrinkSave.Initialize(new LocalStorageProvider(_directory), "saves", ".json", 1); } [TearDown] public void Cleanup() { ShrinkSave.UnregisterModule("test-snapshot"); if (Directory.Exists(_directory)) Directory.Delete(_directory, true); } [TestCase(false)] [TestCase(true)] public async Task CaptureRunsOnCallerAndWorkerEncodingPreservesFormat(bool encrypt) { var owner = Environment.CurrentManagedThreadId; var captured = 0; var value = new SaveDto { Value = "original" }; ShrinkSave.RegisterModule("test-snapshot", () => { captured = Environment.CurrentManagedThreadId; return value; }, _ => { }); var options = new SaveOptions { EncodeInBackground = true, CollectTimings = true, Encrypt = encrypt, EncryptionKey = "test-only" }; var saving = ShrinkSave.SaveSlotAsync(0, options).AsTask(); value.Value = "changed-after-capture"; await saving; var file = Directory.GetFiles(Path.Combine(_directory, "saves"), "*.json").Single(); var packet = DataSerializer.Deserialize(await File.ReadAllBytesAsync(file)); var text = encrypt ? System.Text.Encoding.UTF8.GetString(SaveEncryptor.Decrypt(Convert.FromBase64String(packet.EncryptedModules), "test-only")) : packet.Modules["test-snapshot"].ToString(); Assert.That(captured, Is.EqualTo(owner)); Assert.That(text, Does.Contain("original").And.Not.Contain("changed-after-capture")); Assert.That(options.LastTimings.EncodedBytes, Is.GreaterThan(0)); Assert.That(options.LastTimings.CaptureMilliseconds, Is.GreaterThanOrEqualTo(0)); } [Test] public async Task CancellationLeavesExistingFileAndBackupsRecoverCorruption() { var storage = new LocalStorageProvider(_directory); await storage.WriteAsync("slot.json", new byte[] { 1 }); await storage.WriteAsync("slot.json", new byte[] { 2 }); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); Assert.CatchAsync(async () => await storage.WriteAsync("slot.json", new byte[] { 3 }, cancellation.Token)); Assert.That(await storage.ReadAsync("slot.json"), Is.EqualTo(new byte[] { 2 })); Assert.That(await storage.ReadAsync("slot.json.bak1"), Is.EqualTo(new byte[] { 1 })); Assert.That(File.Exists(Path.Combine(_directory, "slot.json.tmp")), Is.False); } [Test] public async Task CorruptPrimarySaveFallsBackToPreviousValidPacket() { await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "previous" }); await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "latest" }); var primary = Directory.GetFiles(Path.Combine(_directory, "saves"), "*.json").Single(); await File.WriteAllTextAsync(primary, "corrupt"); Assert.That((await ShrinkSave.GetMetaAsync(0)).SlotName, Is.EqualTo("previous")); } [Test] public void RuntimeSnapshotExportsBindingsWithoutSessionSecrets() { var service = new ShrinkNetworkService(); service.RegisterMessage(42, "test/message"); service.RegisterHandler(Receive); var snapshot = new ShrinkInspectionSnapshot("build-a"); snapshot.AddNetwork("server", service); var json = snapshot.ToJson(); Assert.That(snapshot.Relations.Any(r => r.From == "opcode:42" && r.To == "type:CodecMessage")); Assert.That(snapshot.Relations.Any(r => r.To == "M:SaveSnapshotTests.Receive(ShrinkNetwork.ShrinkNetworkContext,CodecMessage)")); Assert.That(json, Does.Not.Contain("Payload").And.Not.Contain("SessionToken")); } private static UniTask Receive(ShrinkNetworkContext ctx, CodecMessage message) => UniTask.CompletedTask; public sealed class SaveDto { public string Value { get; set; } = ""; } }