LANCommander/LANCommander.SDK.Tests/Install/ManifestHelperTests.cs
2026-02-23 19:20:53 -06:00

493 lines
17 KiB
C#

using LANCommander.SDK.Enums;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.Services;
using ManifestGame = LANCommander.SDK.Models.Manifest.Game;
using ManifestAction = LANCommander.SDK.Models.Manifest.Action;
using ManifestRedistributable = LANCommander.SDK.Models.Manifest.Redistributable;
using ManifestGenre = LANCommander.SDK.Models.Manifest.Genre;
using ManifestSavePath = LANCommander.SDK.Models.Manifest.SavePath;
namespace LANCommander.SDK.Tests.Install;
public class ManifestHelperTests : IDisposable
{
private readonly string _tempDir;
public ManifestHelperTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"lc-manifest-tests-{Guid.NewGuid()}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, true);
}
private ManifestGame MakeManifest(Guid? id = null, string title = "Test Game", GameType type = GameType.MainGame)
{
return new ManifestGame
{
Id = id ?? Guid.NewGuid(),
Title = title,
Type = type,
Version = "1.0.0"
};
}
// ── GetPath ───────────────────────────────────────────────────────────────
[Fact]
public void GetPath_ReturnsPathInsideMetadataDirectory()
{
var gameId = Guid.NewGuid();
var expected = GameClient.GetMetadataFilePath(_tempDir, gameId, ManifestHelper.ManifestFilename);
var result = ManifestHelper.GetPath(_tempDir, gameId);
Assert.Equal(expected, result);
}
// ── Exists ────────────────────────────────────────────────────────────────
[Fact]
public void Exists_WhenManifestFileIsAbsent_ReturnsFalse()
{
var result = ManifestHelper.Exists(_tempDir, Guid.NewGuid());
Assert.False(result);
}
[Fact]
public void Exists_WhenManifestFileIsPresent_ReturnsTrue()
{
var manifest = MakeManifest();
ManifestHelper.Write(manifest, _tempDir);
Assert.True(ManifestHelper.Exists(_tempDir, manifest.Id));
}
// ── Write / Read round-trip ───────────────────────────────────────────────
[Fact]
public void Write_CreatesManifestFile()
{
var manifest = MakeManifest();
ManifestHelper.Write(manifest, _tempDir);
var path = ManifestHelper.GetPath(_tempDir, manifest.Id);
Assert.True(File.Exists(path));
}
[Fact]
public void Read_ReturnsNullWhenManifestDoesNotExist()
{
var result = ManifestHelper.Read<ManifestGame>(_tempDir, Guid.NewGuid());
Assert.Null(result);
}
[Fact]
public void WriteAndRead_RoundTripsId()
{
var manifest = MakeManifest();
ManifestHelper.Write(manifest, _tempDir);
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir, manifest.Id);
Assert.NotNull(loaded);
Assert.Equal(manifest.Id, loaded.Id);
}
[Fact]
public void WriteAndRead_RoundTripsTitle()
{
var manifest = MakeManifest(title: "Half-Life 2");
ManifestHelper.Write(manifest, _tempDir);
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir, manifest.Id);
Assert.Equal("Half-Life 2", loaded.Title);
}
[Fact]
public void WriteAndRead_RoundTripsGameType()
{
var manifest = MakeManifest(type: GameType.Expansion);
ManifestHelper.Write(manifest, _tempDir);
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir, manifest.Id);
Assert.Equal(GameType.Expansion, loaded.Type);
}
[Fact]
public void WriteAndRead_RoundTripsVersion()
{
var manifest = MakeManifest();
manifest.Version = "2.3.4";
ManifestHelper.Write(manifest, _tempDir);
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir, manifest.Id);
Assert.Equal("2.3.4", loaded.Version);
}
[Fact]
public void WriteAndRead_RoundTripsActions()
{
var manifest = MakeManifest();
manifest.Actions.Add(new ManifestAction
{
Name = "Play",
Path = "game.exe",
IsPrimaryAction = true,
SortOrder = 0
});
ManifestHelper.Write(manifest, _tempDir);
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir, manifest.Id);
Assert.Single(loaded.Actions);
Assert.Equal("Play", loaded.Actions.First().Name);
Assert.Equal("game.exe", loaded.Actions.First().Path);
Assert.True(loaded.Actions.First().IsPrimaryAction);
}
[Fact]
public void WriteAndRead_RoundTripsAddons()
{
var manifest = MakeManifest();
var addon = MakeManifest(title: "Expansion Pack", type: GameType.Expansion);
manifest.Addons.Add(addon);
ManifestHelper.Write(manifest, _tempDir);
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir, manifest.Id);
Assert.Single(loaded.Addons);
Assert.Equal(addon.Id, loaded.Addons.First().Id);
Assert.Equal("Expansion Pack", loaded.Addons.First().Title);
Assert.Equal(GameType.Expansion, loaded.Addons.First().Type);
}
// ── Async variants ────────────────────────────────────────────────────────
[Fact]
public async Task ReadAsync_ReturnsNullWhenManifestDoesNotExist()
{
var result = await ManifestHelper.ReadAsync<ManifestGame>(_tempDir, Guid.NewGuid());
Assert.Null(result);
}
[Fact]
public async Task WriteAsyncAndReadAsync_RoundTrips()
{
var manifest = MakeManifest(title: "Async Test Game");
await ManifestHelper.WriteAsync(manifest, _tempDir);
var loaded = await ManifestHelper.ReadAsync<ManifestGame>(_tempDir, manifest.Id);
Assert.NotNull(loaded);
Assert.Equal(manifest.Id, loaded.Id);
Assert.Equal("Async Test Game", loaded.Title);
}
// ── Serialize / Deserialize ───────────────────────────────────────────────
[Fact]
public void SerializeAndDeserialize_RoundTripsManifest()
{
var manifest = MakeManifest(title: "Serialization Test");
manifest.Addons.Add(MakeManifest(title: "Addon", type: GameType.Mod));
var yaml = ManifestHelper.Serialize(manifest);
var deserialized = ManifestHelper.Deserialize<ManifestGame>(yaml);
Assert.Equal(manifest.Id, deserialized.Id);
Assert.Equal("Serialization Test", deserialized.Title);
Assert.Single(deserialized.Addons);
Assert.Equal(GameType.Mod, deserialized.Addons.First().Type);
}
[Fact]
public void TryDeserialize_WithValidYaml_ReturnsTrueAndManifest()
{
var manifest = MakeManifest(title: "Valid YAML");
var yaml = ManifestHelper.Serialize(manifest);
var success = ManifestHelper.TryDeserialize<ManifestGame>(yaml, out var result);
Assert.True(success);
Assert.NotNull(result);
Assert.Equal(manifest.Id, result.Id);
}
[Fact]
public void TryDeserialize_WithInvalidYaml_ReturnsFalse()
{
var success = ManifestHelper.TryDeserialize<ManifestGame>("{ invalid yaml [[[", out var result);
Assert.False(success);
Assert.Null(result);
}
[Fact]
public void TryDeserialize_WithEmptyString_ReturnsTrueWithNullManifest()
{
// YamlDotNet treats an empty document as a null value — it doesn't throw,
// so TryDeserialize returns true with a null out-parameter.
var success = ManifestHelper.TryDeserialize<ManifestGame>("", out var result);
Assert.True(success);
Assert.Null(result);
}
// ── Serialize format ──────────────────────────────────────────────────────
[Fact]
public void Serialize_ProducesPascalCaseYaml()
{
var manifest = MakeManifest(title: "Format Check");
var yaml = ManifestHelper.Serialize(manifest);
// PascalCase naming convention means field names match the C# property names
Assert.Contains("Id:", yaml);
Assert.Contains("Title:", yaml);
Assert.Contains("Version:", yaml);
}
[Fact]
public void Serialize_IsDeterministic()
{
var manifest = MakeManifest(title: "Stable Output");
var yaml1 = ManifestHelper.Serialize(manifest);
var yaml2 = ManifestHelper.Serialize(manifest);
Assert.Equal(yaml1, yaml2);
}
// ── Deserialize IgnoreUnmatchedProperties ─────────────────────────────────
[Fact]
public void Deserialize_WithUnknownProperties_DoesNotThrow()
{
var yaml = """
Id: 00000000-0000-0000-0000-000000000001
Title: Known Title
UnknownField: some value
AnotherUnknownField: 42
""";
var ex = Record.Exception(() => ManifestHelper.Deserialize<ManifestGame>(yaml));
Assert.Null(ex);
}
[Fact]
public void Deserialize_WithUnknownProperties_StillPopulatesKnownFields()
{
var yaml = """
Id: 00000000-0000-0000-0000-000000000002
Title: Known Title
UnknownField: ignored
""";
var result = ManifestHelper.Deserialize<ManifestGame>(yaml);
Assert.Equal(new Guid("00000000-0000-0000-0000-000000000002"), result.Id);
Assert.Equal("Known Title", result.Title);
}
// ── Read<T> without Guid (root Manifest.yml) ─────────────────────────────
[Fact]
public void Read_WithoutId_ReadsManifestFromRootOfInstallDirectory()
{
var manifest = MakeManifest(title: "Root Manifest");
var rootPath = Path.Combine(_tempDir, ManifestHelper.ManifestFilename);
File.WriteAllText(rootPath, ManifestHelper.Serialize(manifest));
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir);
Assert.NotNull(loaded);
Assert.Equal(manifest.Id, loaded.Id);
Assert.Equal("Root Manifest", loaded.Title);
}
[Fact]
public void Read_WithoutId_WhenFileNotFound_ThrowsFileNotFoundException()
{
// Unlike Read(installDir, Guid) which returns null, the no-Guid overload
// calls File.ReadAllText directly and throws if the file is absent.
Assert.Throws<FileNotFoundException>(() =>
ManifestHelper.Read<ManifestGame>(_tempDir));
}
// ── ReadAsync<T> without Guid (root Manifest.yml) ─────────────────────────
[Fact]
public async Task ReadAsync_WithoutId_ReadsManifestFromRootOfInstallDirectory()
{
var manifest = MakeManifest(title: "Async Root Manifest");
var rootPath = Path.Combine(_tempDir, ManifestHelper.ManifestFilename);
await File.WriteAllTextAsync(rootPath, ManifestHelper.Serialize(manifest));
var loaded = await ManifestHelper.ReadAsync<ManifestGame>(_tempDir);
Assert.NotNull(loaded);
Assert.Equal(manifest.Id, loaded.Id);
Assert.Equal("Async Root Manifest", loaded.Title);
}
[Fact]
public async Task ReadAsync_WithoutId_WhenFileNotFound_ThrowsFileNotFoundException()
{
await Assert.ThrowsAsync<FileNotFoundException>(() =>
ManifestHelper.ReadAsync<ManifestGame>(_tempDir));
}
// ── Write / WriteAsync return values ─────────────────────────────────────
[Fact]
public void Write_ReturnsPathToWrittenFile()
{
var manifest = MakeManifest();
var returnedPath = ManifestHelper.Write(manifest, _tempDir);
var expectedPath = ManifestHelper.GetPath(_tempDir, manifest.Id);
Assert.Equal(expectedPath, returnedPath);
Assert.True(File.Exists(returnedPath));
}
[Fact]
public async Task WriteAsync_ReturnsPathToWrittenFile()
{
var manifest = MakeManifest();
var returnedPath = await ManifestHelper.WriteAsync(manifest, _tempDir);
var expectedPath = ManifestHelper.GetPath(_tempDir, manifest.Id);
Assert.Equal(expectedPath, returnedPath);
Assert.True(File.Exists(returnedPath));
}
// ── Write overwrites ──────────────────────────────────────────────────────
[Fact]
public void Write_WhenCalledTwice_OverwritesExistingManifest()
{
var id = Guid.NewGuid();
var first = MakeManifest(id: id, title: "First Title");
var second = MakeManifest(id: id, title: "Second Title");
ManifestHelper.Write(first, _tempDir);
ManifestHelper.Write(second, _tempDir);
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir, id);
Assert.Equal("Second Title", loaded.Title);
}
// ── Exists after WriteAsync ───────────────────────────────────────────────
[Fact]
public async Task Exists_AfterWriteAsync_ReturnsTrue()
{
var manifest = MakeManifest();
await ManifestHelper.WriteAsync(manifest, _tempDir);
Assert.True(ManifestHelper.Exists(_tempDir, manifest.Id));
}
// ── GetPath structure ─────────────────────────────────────────────────────
[Fact]
public void GetPath_PathContainsLanCommanderSubdirectory()
{
var result = ManifestHelper.GetPath(_tempDir, Guid.NewGuid());
Assert.Contains(".lancommander", result);
}
[Fact]
public void GetPath_PathContainsGameId()
{
var id = Guid.NewGuid();
var result = ManifestHelper.GetPath(_tempDir, id);
Assert.Contains(id.ToString(), result);
}
[Fact]
public void GetPath_PathEndsWithManifestFilename()
{
var result = ManifestHelper.GetPath(_tempDir, Guid.NewGuid());
Assert.Equal(ManifestHelper.ManifestFilename, Path.GetFileName(result));
}
// ── Write/Read with non-Game IKeyedModel ──────────────────────────────────
[Fact]
public void WriteAndRead_WithRedistributableManifest_RoundTrips()
{
var redist = new ManifestRedistributable
{
Id = Guid.NewGuid(),
Name = "DirectX Runtime",
Description = "Required runtime libraries"
};
ManifestHelper.Write(redist, _tempDir);
var loaded = ManifestHelper.Read<ManifestRedistributable>(_tempDir, redist.Id);
Assert.NotNull(loaded);
Assert.Equal(redist.Id, loaded.Id);
Assert.Equal("DirectX Runtime", loaded.Name);
Assert.Equal("Required runtime libraries", loaded.Description);
}
// ── Round-trips for richer collection fields ──────────────────────────────
[Fact]
public void WriteAndRead_RoundTripsGenres()
{
var manifest = MakeManifest();
manifest.Genres.Add(new ManifestGenre { Name = "Action" });
manifest.Genres.Add(new ManifestGenre { Name = "RPG" });
ManifestHelper.Write(manifest, _tempDir);
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir, manifest.Id);
Assert.Equal(2, loaded.Genres.Count);
Assert.Contains(loaded.Genres, g => g.Name == "Action");
Assert.Contains(loaded.Genres, g => g.Name == "RPG");
}
[Fact]
public void WriteAndRead_RoundTripsSavePaths()
{
var manifest = MakeManifest();
manifest.SavePaths.Add(new ManifestSavePath
{
Id = Guid.NewGuid(),
Path = "%APPDATA%\\MyGame\\Saves",
Type = SavePathType.File,
IsRegex = false
});
ManifestHelper.Write(manifest, _tempDir);
var loaded = ManifestHelper.Read<ManifestGame>(_tempDir, manifest.Id);
Assert.Single(loaded.SavePaths);
Assert.Equal("%APPDATA%\\MyGame\\Saves", loaded.SavePaths.First().Path);
Assert.Equal(SavePathType.File, loaded.SavePaths.First().Type);
Assert.False(loaded.SavePaths.First().IsRegex);
}
}