Files
the-living-world/tests/TheLivingWorld.Tests/WorldGenerationServiceTests.cs
T

139 lines
4.9 KiB
C#

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using TheLivingWorld.Api.Generation;
using TheLivingWorld.Api.Simulation;
using TheLivingWorld.Api.Storage;
using TheLivingWorld.Core.Contracts;
using TheLivingWorld.Core.Export;
using TheLivingWorld.Osm;
using TheLivingWorld.Osm.Import;
using TheLivingWorld.Osm.Overpass;
namespace TheLivingWorld.Tests;
public sealed class WorldGenerationServiceTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), $"tlw-gen-{Guid.NewGuid():n}");
private readonly WorldStore _store;
public WorldGenerationServiceTests()
{
_store = new WorldStore(
Options.Create(new WorldStorageOptions { RootDirectory = _root }),
NullLogger<WorldStore>.Instance);
}
[Fact]
public async Task StartAsync_rejects_creation_when_the_slot_budget_is_full()
{
await _store.SaveSummaryAsync(Summary("taken-aaaaaaaa", "Taken"));
using var service = CreateService(maxConcurrentWorlds: 1);
var failure = await Assert.ThrowsAsync<WorldCapacityExceededException>(() =>
service.StartAsync(new CreateWorldRequest
{
Name = "Overflow",
Latitude = 31.8966010,
Longitude = -100.4858591,
SizeKm = 10,
}, CancellationToken.None));
Assert.Equal(1, failure.MaxConcurrentWorlds);
Assert.Equal(1, await _store.CountAsync());
Assert.Equal("Taken", Assert.Single(await _store.ListAsync()).Name);
}
[Fact]
public async Task StartAsync_uses_requested_start_game_time_on_the_pending_clock()
{
using var service = CreateService(maxConcurrentWorlds: 2);
var start = new DateTime(1995, 6, 15, 8, 30, 0, DateTimeKind.Unspecified);
var summary = await service.StartAsync(new CreateWorldRequest
{
Name = "Custom clock",
Latitude = 31.8966010,
Longitude = -100.4858591,
SizeKm = 5,
StartGameTime = start,
}, CancellationToken.None);
Assert.NotNull(summary.Clock);
Assert.Equal(start, summary.Clock.GameTime);
var stored = await _store.GetSummaryAsync(summary.Id);
Assert.NotNull(stored?.Clock);
Assert.Equal(start, stored.Clock.GameTime);
}
private WorldGenerationService CreateService(int maxConcurrentWorlds)
{
// The capacity check runs before any Overpass work, so this generator is never invoked by the
// rejection test. The acceptance test only asserts the pending summary was written.
var generator = new OsmWorldGenerator(
new OverpassClient(
new HttpClient(new UnreachableHandler()),
Options.Create(new OsmOptions
{
Endpoints = ["https://unreachable.example/api"],
CacheDirectory = Path.Combine(_root, "osm-cache"),
MaxAttemptsPerEndpoint = 1,
RequestTimeoutSeconds = 1,
}),
NullLogger<OverpassClient>.Instance),
new OsmWorldBuilder(NullLogger<OsmWorldBuilder>.Instance),
NullLogger<OsmWorldGenerator>.Instance);
return new WorldGenerationService(
generator,
_store,
new ChunkExporter(),
new WorldSimulationHost(_store, NullLogger<WorldSimulationHost>.Instance),
Options.Create(new WorldStorageOptions
{
RootDirectory = _root,
MaxConcurrentWorlds = maxConcurrentWorlds,
}),
new NeverStoppingLifetime(),
NullLogger<WorldGenerationService>.Instance);
}
private static WorldSummaryDto 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);
}
private sealed class NeverStoppingLifetime : IHostApplicationLifetime
{
public CancellationToken ApplicationStarted => CancellationToken.None;
public CancellationToken ApplicationStopped => CancellationToken.None;
public CancellationToken ApplicationStopping => CancellationToken.None;
public void StopApplication()
{
}
}
private sealed class UnreachableHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken) =>
throw new InvalidOperationException("Overpass must not be contacted by these tests.");
}
}