new progression.adapters project, no engine reference. Stores take a directory path; Godot resolves user:// with GlobalizePath and passes it in, which is what makes them testable at all - originally I'd written this slice off as untestable. version lives in a file envelope, not the snapshot, so the domain only knows the current shape. A newer-build save reports Corrupt with a reason instead of masquerading as "no save" and getting overwritten. atomic write via temp + rename. One file per aggregate: saving a run leaves the profile file byte-identical, asserted. guard rail fixed - it was matching prose in doc comments, now anchored to using-directives plus a project.assets.json check. 63 tests.
122 lines
4.8 KiB
C#
122 lines
4.8 KiB
C#
using Mr.BrickAdventures.Progression.Adapters;
|
|
|
|
namespace Mr.BrickAdventures.Progression.Tests;
|
|
|
|
/// <summary>
|
|
/// These run against a real directory on disk. The store takes a path rather than using Godot's
|
|
/// FileAccess, which is the whole reason this is testable at all.
|
|
/// </summary>
|
|
public sealed class JsonPlaythroughStoreTests : IDisposable
|
|
{
|
|
private readonly string _directory =
|
|
Path.Combine(Path.GetTempPath(), "ppc-progression-" + Guid.NewGuid().ToString("N"));
|
|
|
|
private static readonly LevelId First = LevelId.Of("village_1");
|
|
private static readonly LevelId Second = LevelId.Of("village_2");
|
|
|
|
private static readonly PlaythroughId Id =
|
|
PlaythroughId.Of(new Guid("55555555-5555-5555-5555-555555555555"));
|
|
|
|
private static Campaign TwoLevels() => Campaign.Of(new[] { First, Second });
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_playthrough_survives_a_round_trip_through_a_real_file()
|
|
{
|
|
var store = new JsonPlaythroughStore(_directory);
|
|
var campaign = TwoLevels();
|
|
var playthrough = Playthrough.StartNew(Id, campaign, Lives.Of(3));
|
|
var attempt = new Attempt(First);
|
|
attempt.RecordCoinsFound(Coins.Of(50));
|
|
attempt.TryRecordPurchase(SkillId.Of("brick_throw"), SkillLevel.Of(2), Coins.Of(30));
|
|
playthrough.SettleCompletion(attempt, campaign);
|
|
|
|
store.Save(playthrough);
|
|
var result = store.Load(Id);
|
|
|
|
Assert.Equal(LoadOutcome.Loaded, result.Outcome);
|
|
Assert.Equal(Coins.Of(20), result.Value!.Bank);
|
|
Assert.Equal(Second, result.Value.CurrentLevel);
|
|
Assert.Equal(SkillLevel.Of(2), result.Value.OwnedSkills[SkillId.Of("brick_throw")]);
|
|
Assert.True(result.Value.HasCleared(First));
|
|
}
|
|
|
|
[Fact]
|
|
public void No_file_means_absent()
|
|
{
|
|
Assert.Equal(LoadOutcome.Absent, new JsonPlaythroughStore(_directory).Load(Id).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Unreadable_json_is_corrupt_not_absent()
|
|
{
|
|
var store = new JsonPlaythroughStore(_directory);
|
|
store.Save(Playthrough.StartNew(Id, TwoLevels(), Lives.Of(3)));
|
|
File.WriteAllText(Directory.GetFiles(_directory, "*.json").Single(), "{ this is not json");
|
|
|
|
var result = store.Load(Id);
|
|
|
|
Assert.Equal(LoadOutcome.Corrupt, result.Outcome);
|
|
Assert.NotNull(result.Problem);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_save_from_a_newer_build_is_corrupt_rather_than_treated_as_no_save()
|
|
{
|
|
// The bug this replaces, exactly: SaveSystem compared versions with != and returned false,
|
|
// ContinueGame read false as "no save", called StartNewGame, and overwrote the file. Here the
|
|
// caller can tell the difference and say so instead.
|
|
var store = new JsonPlaythroughStore(_directory);
|
|
store.Save(Playthrough.StartNew(Id, TwoLevels(), Lives.Of(3)));
|
|
var file = Directory.GetFiles(_directory, "*.json").Single();
|
|
File.WriteAllText(file, File.ReadAllText(file).Replace("\"version\": 1", "\"version\": 99"));
|
|
|
|
var result = store.Load(Id);
|
|
|
|
Assert.Equal(LoadOutcome.Corrupt, result.Outcome);
|
|
Assert.Contains("newer build", result.Problem);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_snapshot_the_domain_rejects_is_corrupt()
|
|
{
|
|
var store = new JsonPlaythroughStore(_directory);
|
|
store.Save(Playthrough.StartNew(Id, TwoLevels(), Lives.Of(3)));
|
|
var file = Directory.GetFiles(_directory, "*.json").Single();
|
|
File.WriteAllText(file, File.ReadAllText(file).Replace("\"lives\": 3", "\"lives\": -1"));
|
|
|
|
Assert.Equal(LoadOutcome.Corrupt, store.Load(Id).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Saving_leaves_no_temporary_file_behind()
|
|
{
|
|
var store = new JsonPlaythroughStore(_directory);
|
|
store.Save(Playthrough.StartNew(Id, TwoLevels(), Lives.Of(3)));
|
|
store.Save(Playthrough.StartNew(Id, TwoLevels(), Lives.Of(2)));
|
|
|
|
Assert.Empty(Directory.GetFiles(_directory, "*.tmp"));
|
|
Assert.Single(Directory.GetFiles(_directory, "*.json"));
|
|
Assert.Equal(2, store.Load(Id).Value!.Lives.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Each_playthrough_gets_its_own_file_so_slots_do_not_disturb_each_other()
|
|
{
|
|
// The reason for a flat record per aggregate rather than one combined document.
|
|
var other = PlaythroughId.Of(new Guid("66666666-6666-6666-6666-666666666666"));
|
|
var store = new JsonPlaythroughStore(_directory);
|
|
|
|
store.Save(Playthrough.StartNew(Id, TwoLevels(), Lives.Of(3)));
|
|
store.Save(Playthrough.StartNew(other, TwoLevels(), Lives.Of(1)));
|
|
|
|
Assert.Equal(2, Directory.GetFiles(_directory, "*.json").Length);
|
|
Assert.Equal(3, store.Load(Id).Value!.Lives.Value);
|
|
Assert.Equal(1, store.Load(other).Value!.Lives.Value);
|
|
}
|
|
}
|