LANCommander/LANCommander.SDK.Tests/Utilities/SavePackerTests.cs
2026-02-24 00:42:39 -06:00

404 lines
14 KiB
C#

using LANCommander.SDK.Enums;
using LANCommander.SDK.Utilities;
using System.IO.Compression;
using ManifestGame = LANCommander.SDK.Models.Manifest.Game;
using ManifestSavePath = LANCommander.SDK.Models.Manifest.SavePath;
namespace LANCommander.SDK.Tests.Utilities;
public class SavePackerTests : IDisposable
{
private readonly string _tempDir;
public SavePackerTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"lc-savepacker-{Guid.NewGuid()}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, true);
}
// Creates a SavePath whose resolved paths are relative to _tempDir.
// workingDirectory defaults to "{InstallDir}" so that ExpandEnvironmentVariables
// resolves it to _tempDir, making Path.Combine(_tempDir, path) the effective target.
private ManifestSavePath MakeSavePath(
SavePathType type,
string path,
string workingDirectory = "{InstallDir}",
bool isRegex = false) => new()
{
Id = Guid.NewGuid(),
Type = type,
Path = path,
WorkingDirectory = workingDirectory,
IsRegex = isRegex
};
private void WriteFile(string relativePath, string content = "data")
{
var full = Path.Combine(_tempDir, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(full)!);
File.WriteAllText(full, content);
}
// Open the packed zip and return all entry names normalised to forward slashes.
private static List<string> EntryKeys(Stream stream)
{
stream.Position = 0;
using var zip = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true);
return zip.Entries.Select(e => e.FullName.Replace('\\', '/')).ToList();
}
// ── Initial state ─────────────────────────────────────────────────────────
[Fact]
public void HasEntries_OnNewPacker_ReturnsFalse()
{
using var packer = new SavePacker(_tempDir);
Assert.False(packer.HasEntries());
}
[Fact]
public void HasManifest_OnNewPacker_ReturnsFalse()
{
using var packer = new SavePacker(_tempDir);
Assert.False(packer.HasManifest());
}
// ── AddManifestAsync ──────────────────────────────────────────────────────
[Fact]
public async Task AddManifestAsync_SetsHasManifestTrue()
{
using var packer = new SavePacker(_tempDir);
await packer.AddManifestAsync(new ManifestGame { Id = Guid.NewGuid(), Title = "Test" });
Assert.True(packer.HasManifest());
}
[Fact]
public async Task AddManifestAsync_SetsHasEntriesTrue()
{
using var packer = new SavePacker(_tempDir);
await packer.AddManifestAsync(new ManifestGame { Id = Guid.NewGuid(), Title = "Test" });
Assert.True(packer.HasEntries());
}
[Fact]
public async Task AddManifestAsync_PackedArchiveContainsManifestYml()
{
using var packer = new SavePacker(_tempDir);
await packer.AddManifestAsync(new ManifestGame { Id = Guid.NewGuid(), Title = "Test" });
var stream = await packer.PackAsync();
var keys = EntryKeys(stream);
Assert.Contains(keys, k => string.Equals(k, "Manifest.yml", StringComparison.OrdinalIgnoreCase));
}
// ── PackAsync ─────────────────────────────────────────────────────────────
[Fact]
public async Task PackAsync_ReturnsStreamAtPositionZero()
{
using var packer = new SavePacker(_tempDir);
var stream = await packer.PackAsync();
Assert.Equal(0, stream.Position);
}
[Fact]
public async Task PackAsync_StreamCanBeOpenedAsZip()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
packer.AddPath(MakeSavePath(SavePathType.File, "save.dat"));
var stream = await packer.PackAsync();
var ex = Record.Exception(() =>
{
stream.Position = 0;
using var zip = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true);
_ = zip.Entries.Count;
});
Assert.Null(ex);
}
// ── Fluent interface ──────────────────────────────────────────────────────
[Fact]
public void AddPath_ReturnsSamePacker()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
var result = packer.AddPath(MakeSavePath(SavePathType.File, "save.dat"));
Assert.Same(packer, result);
}
[Fact]
public void AddPaths_ReturnsSamePacker()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
var result = packer.AddPaths([MakeSavePath(SavePathType.File, "save.dat")]);
Assert.Same(packer, result);
}
// ── Argument / type validation ────────────────────────────────────────────
[Fact]
public void AddPath_WithUnknownSavePathType_ThrowsArgumentOutOfRangeException()
{
using var packer = new SavePacker(_tempDir);
Assert.Throws<ArgumentOutOfRangeException>(() =>
packer.AddPath(MakeSavePath((SavePathType)99, "irrelevant")));
}
// ── AddRegistryPath guard ─────────────────────────────────────────────────
[Fact]
public void AddRegistryPath_WithNonRegistryType_AddsNoEntries()
{
using var packer = new SavePacker(_tempDir);
var filePath = MakeSavePath(SavePathType.File, "save.dat");
packer.AddRegistryPath(filePath);
Assert.False(packer.HasEntries());
}
// ── AddPath: single file ──────────────────────────────────────────────────
[Fact]
public void AddPath_WithExistingFile_SetsHasEntriesTrue()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
packer.AddPath(MakeSavePath(SavePathType.File, "save.dat"));
Assert.True(packer.HasEntries());
}
[Fact]
public async Task AddPath_WithExistingFile_EntryKeyStartsWithFilesPrefix()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
packer.AddPath(MakeSavePath(SavePathType.File, "save.dat"));
var keys = EntryKeys(await packer.PackAsync());
Assert.All(keys, k => Assert.StartsWith("Files/", k));
}
[Fact]
public async Task AddPath_WithExistingFile_EntryKeyContainsSavePathId()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
var savePath = MakeSavePath(SavePathType.File, "save.dat");
packer.AddPath(savePath);
var keys = EntryKeys(await packer.PackAsync());
Assert.Contains(keys, k => k.Contains(savePath.Id.ToString()));
}
[Fact]
public async Task AddPath_WithExistingFile_EntryKeyEndsWithFilename()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
packer.AddPath(MakeSavePath(SavePathType.File, "save.dat"));
var keys = EntryKeys(await packer.PackAsync());
Assert.Contains(keys, k => k.EndsWith("save.dat"));
}
[Fact]
public void AddPath_WithNonExistentPath_AddsNoEntries()
{
using var packer = new SavePacker(_tempDir);
// "missing.dat" is never created on disk
packer.AddPath(MakeSavePath(SavePathType.File, "missing.dat"));
Assert.False(packer.HasEntries());
}
// ── AddPath: directory ────────────────────────────────────────────────────
[Fact]
public async Task AddPath_WithDirectory_AddsAllContainedFiles()
{
using var packer = new SavePacker(_tempDir);
WriteFile("saves/slot1.dat");
WriteFile("saves/slot2.dat");
WriteFile("saves/slot3.dat");
packer.AddPath(MakeSavePath(SavePathType.File, "saves"));
var keys = EntryKeys(await packer.PackAsync());
Assert.Equal(3, keys.Count);
}
[Fact]
public async Task AddPath_WithDirectory_EntryKeysContainEachFilename()
{
using var packer = new SavePacker(_tempDir);
WriteFile("saves/slot1.dat");
WriteFile("saves/slot2.dat");
packer.AddPath(MakeSavePath(SavePathType.File, "saves"));
var keys = EntryKeys(await packer.PackAsync());
Assert.Contains(keys, k => k.EndsWith("slot1.dat"));
Assert.Contains(keys, k => k.EndsWith("slot2.dat"));
}
[Fact]
public async Task AddPath_WithDirectory_RecursivelyAddsNestedFiles()
{
using var packer = new SavePacker(_tempDir);
WriteFile("saves/slot1.dat");
WriteFile("saves/sub/slot2.dat");
packer.AddPath(MakeSavePath(SavePathType.File, "saves"));
var keys = EntryKeys(await packer.PackAsync());
Assert.Equal(2, keys.Count);
Assert.Contains(keys, k => k.EndsWith("slot2.dat"));
}
// ── AddPath: regex pattern ────────────────────────────────────────────────
[Fact]
public async Task AddPath_WithRegexPattern_AddsOnlyMatchingFiles()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save1.dat");
WriteFile("save2.dat");
WriteFile("config.ini");
packer.AddPath(MakeSavePath(SavePathType.File, @"\.dat$", isRegex: true));
var keys = EntryKeys(await packer.PackAsync());
Assert.Equal(2, keys.Count);
Assert.All(keys, k => Assert.EndsWith(".dat", k));
}
[Fact]
public async Task AddPath_WithRegexPattern_ExcludesNonMatchingFiles()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
WriteFile("config.ini");
packer.AddPath(MakeSavePath(SavePathType.File, @"\.dat$", isRegex: true));
var keys = EntryKeys(await packer.PackAsync());
Assert.DoesNotContain(keys, k => k.EndsWith(".ini"));
}
// ── AddPaths ──────────────────────────────────────────────────────────────
[Fact]
public void AddPaths_WithEmptyCollection_AddsNoEntries()
{
using var packer = new SavePacker(_tempDir);
packer.AddPaths([]);
Assert.False(packer.HasEntries());
}
[Fact]
public async Task AddPaths_WithMultiplePaths_AddsFilesFromEach()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
WriteFile("config.ini");
packer.AddPaths([
MakeSavePath(SavePathType.File, "save.dat"),
MakeSavePath(SavePathType.File, "config.ini")
]);
var keys = EntryKeys(await packer.PackAsync());
Assert.Equal(2, keys.Count);
Assert.Contains(keys, k => k.EndsWith("save.dat"));
Assert.Contains(keys, k => k.EndsWith("config.ini"));
}
[Fact]
public async Task AddPaths_TwoDifferentSavePathIds_ProducesEntriesUnderDistinctPrefixes()
{
using var packer = new SavePacker(_tempDir);
WriteFile("slot1.dat");
WriteFile("slot2.dat");
var path1 = MakeSavePath(SavePathType.File, "slot1.dat");
var path2 = MakeSavePath(SavePathType.File, "slot2.dat");
packer.AddPaths([path1, path2]);
var keys = EntryKeys(await packer.PackAsync());
Assert.Contains(keys, k => k.Contains(path1.Id.ToString()));
Assert.Contains(keys, k => k.Contains(path2.Id.ToString()));
}
// ── Manifest + files together ─────────────────────────────────────────────
[Fact]
public async Task PackAsync_WithManifestAndFile_ContainsBothEntries()
{
using var packer = new SavePacker(_tempDir);
WriteFile("save.dat");
await packer.AddManifestAsync(new ManifestGame { Id = Guid.NewGuid(), Title = "Test" });
packer.AddPath(MakeSavePath(SavePathType.File, "save.dat"));
var keys = EntryKeys(await packer.PackAsync());
Assert.Contains(keys, k => string.Equals(k, "Manifest.yml", StringComparison.OrdinalIgnoreCase));
Assert.Contains(keys, k => k.EndsWith("save.dat"));
}
// ── Dispose ───────────────────────────────────────────────────────────────
[Fact]
public void Dispose_DoesNotThrow()
{
var packer = new SavePacker(_tempDir);
var ex = Record.Exception(() => packer.Dispose());
Assert.Null(ex);
}
[Fact]
public void Using_Block_DisposesWithoutThrowing()
{
var ex = Record.Exception(() =>
{
using var packer = new SavePacker(_tempDir);
});
Assert.Null(ex);
}
}