using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using TheLivingWorld.Api.Storage; using TheLivingWorld.Core.Contracts; namespace TheLivingWorld.Tests; public sealed class WorldStoreTests : IDisposable { private readonly string _root = Path.Combine(Path.GetTempPath(), $"tlw-worlds-{Guid.NewGuid():n}"); private readonly WorldStore _store; public WorldStoreTests() { _store = new WorldStore( Options.Create(new WorldStorageOptions { RootDirectory = _root }), NullLogger.Instance); } [Fact] public async Task CountAsync_is_zero_when_the_root_is_missing() { Assert.Equal(0, await _store.CountAsync()); Assert.Empty(await _store.ListAsync()); } [Fact] public async Task CountAsync_and_ListAsync_track_saved_summaries() { await _store.SaveSummaryAsync(Summary("alpha-11111111", "Alpha")); await _store.SaveSummaryAsync(Summary("bravo-22222222", "Bravo")); Assert.Equal(2, await _store.CountAsync()); var listed = await _store.ListAsync(); Assert.Equal(2, listed.Count); Assert.Equal(["Bravo", "Alpha"], listed.Select(world => world.Name).ToArray()); } [Fact] public async Task CountAsync_ignores_directories_without_readable_state() { await _store.SaveSummaryAsync(Summary("alpha-11111111", "Alpha")); Directory.CreateDirectory(Path.Combine(_root, "not-a-valid-id!!!")); Directory.CreateDirectory(Path.Combine(_root, "orphan-33333333")); Assert.Equal(1, await _store.CountAsync()); } [Fact] public async Task TryUpdateSummaryAsync_writes_over_an_existing_state_file() { var summary = Summary("alpha-11111111", "Alpha"); await _store.SaveSummaryAsync(summary); Assert.True(await _store.TryUpdateSummaryAsync(summary with { Name = "Renamed" })); Assert.Equal("Renamed", (await _store.GetSummaryAsync(summary.Id))?.Name); } [Fact] public async Task TryUpdateSummaryAsync_refuses_to_recreate_a_deleted_world() { var summary = Summary("alpha-11111111", "Alpha"); await _store.SaveSummaryAsync(summary); Assert.True(_store.Delete(summary.Id)); Assert.False(await _store.TryUpdateSummaryAsync(summary)); Assert.False(Directory.Exists(Path.Combine(_root, summary.Id))); Assert.Equal(0, await _store.CountAsync()); } [Fact] public async Task SaveSummaryAsync_leaves_no_scratch_files_behind() { var summary = Summary("alpha-11111111", "Alpha"); await _store.SaveSummaryAsync(summary); await _store.SaveSummaryAsync(summary with { Name = "Alpha again" }); var files = Directory.GetFiles(Path.Combine(_root, summary.Id)); Assert.Equal(["state.json"], files.Select(static path => Path.GetFileName(path)).Order().ToArray()!); } private static StoredWorldDto Summary(string id, string name) => new() { Id = id, Name = name, Latitude = 31.9, Longitude = -100.5, SizeMeters = 10_000, Status = WorldStatus.Ready, CreatedAt = DateTimeOffset.UtcNow, }; public void Dispose() { if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true); GC.SuppressFinalize(this); } }