neon-sprawl/server/NeonSprawl.Server.Tests/Game/Contracts/InMemoryContractInstanceSto...

356 lines
11 KiB
C#

using NeonSprawl.Server.Game.Contracts;
using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Tests.Game.Contracts;
public sealed class InMemoryContractInstanceStoreTests
{
private const string PlayerId = "dev-local-1";
private const string UnknownPlayerId = "unknown-player-xyz";
private const string InstanceId = "prototype_contract_instance_001";
private const string SecondInstanceId = "prototype_contract_instance_002";
private const string TemplateId = "prototype_contract_clear_combat_pocket";
private const string SeedBucket = "2026-06-22";
private static readonly DateTimeOffset IssuedAt = new(2026, 6, 22, 10, 0, 0, TimeSpan.Zero);
private static readonly DateTimeOffset CompletedAt = new(2026, 6, 22, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void TryGet_ShouldReturnFalse_WhenRowMissing()
{
// Arrange
var store = CreateStore();
// Act
var found = store.TryGet(PlayerId, InstanceId, out _);
// Assert
Assert.False(found);
}
[Fact]
public void TryCreateActive_ShouldPersistAllFields()
{
// Arrange
var store = CreateStore();
// Act
var created = store.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out var snapshot);
// Assert
Assert.True(created);
Assert.Equal(InstanceId, snapshot.ContractInstanceId);
Assert.Equal(TemplateId, snapshot.TemplateId);
Assert.Equal(PlayerId, snapshot.PlayerId);
Assert.Equal(ContractInstanceStatus.Active, snapshot.Status);
Assert.Equal(SeedBucket, snapshot.SeedBucket);
Assert.Equal(IssuedAt, snapshot.IssuedAt);
Assert.Null(snapshot.CompletedAt);
}
[Fact]
public void TryCreateActive_ShouldDenyDuplicateInstanceId()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out var firstSnapshot));
// Act
var duplicate = store.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt.AddHours(1),
out var secondSnapshot);
// Assert
Assert.False(duplicate);
Assert.Equal(firstSnapshot.ContractInstanceId, secondSnapshot.ContractInstanceId);
Assert.Equal(firstSnapshot.IssuedAt, secondSnapshot.IssuedAt);
Assert.Equal(ContractInstanceStatus.Active, secondSnapshot.Status);
}
[Fact]
public void TryCreateActive_ShouldDenySecondActiveForSamePlayer()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out _));
// Act
var second = store.TryCreateActive(
PlayerId,
SecondInstanceId,
TemplateId,
SeedBucket,
IssuedAt.AddHours(1),
out var snapshot);
// Assert
Assert.False(second);
Assert.Equal(InstanceId, snapshot.ContractInstanceId);
Assert.Equal(ContractInstanceStatus.Active, snapshot.Status);
Assert.True(store.TryGetActiveForPlayer(PlayerId, out var active));
Assert.Equal(InstanceId, active.ContractInstanceId);
}
[Fact]
public void TryMarkComplete_ShouldSetCompletedStatusAndTimestamp()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out _));
// Act
var completed = store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out var snapshot);
// Assert
Assert.True(completed);
Assert.Equal(ContractInstanceStatus.Completed, snapshot.Status);
Assert.Equal(CompletedAt, snapshot.CompletedAt);
Assert.False(store.TryGetActiveForPlayer(PlayerId, out _));
}
[Fact]
public void TryMarkComplete_ShouldBeIdempotent()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out _));
// Act
var first = store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out var firstSnapshot);
var second = store.TryMarkComplete(
PlayerId,
InstanceId,
CompletedAt.AddHours(1),
out var secondSnapshot);
// Assert
Assert.True(first);
Assert.False(second);
Assert.Equal(ContractInstanceStatus.Completed, secondSnapshot.Status);
Assert.Equal(CompletedAt, secondSnapshot.CompletedAt);
Assert.Equal(firstSnapshot.TemplateId, secondSnapshot.TemplateId);
}
[Fact]
public void TryCreateActive_ShouldAllowNewInstanceAfterComplete()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out _));
Assert.True(store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out _));
// Act
var reissued = store.TryCreateActive(
PlayerId,
SecondInstanceId,
TemplateId,
SeedBucket,
IssuedAt.AddDays(1),
out var snapshot);
// Assert
Assert.True(reissued);
Assert.Equal(SecondInstanceId, snapshot.ContractInstanceId);
Assert.Equal(ContractInstanceStatus.Active, snapshot.Status);
}
[Fact]
public void TryCreateActive_ShouldReturnFalse_ForUnknownPlayer()
{
// Arrange
var store = CreateStore();
// Act
var created = store.TryCreateActive(
UnknownPlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out _);
// Assert
Assert.False(created);
}
[Theory]
[InlineData("", InstanceId, TemplateId, SeedBucket)]
[InlineData(" ", InstanceId, TemplateId, SeedBucket)]
[InlineData(PlayerId, "", TemplateId, SeedBucket)]
[InlineData(PlayerId, " ", TemplateId, SeedBucket)]
[InlineData(PlayerId, InstanceId, "", SeedBucket)]
[InlineData(PlayerId, InstanceId, TemplateId, "")]
public void TryCreateActive_ShouldReturnFalse_WhenRequiredIdsEmpty(
string playerId,
string instanceId,
string templateId,
string seedBucket)
{
// Arrange
var store = CreateStore();
// Act
var created = store.TryCreateActive(
playerId,
instanceId,
templateId,
seedBucket,
IssuedAt,
out _);
// Assert
Assert.False(created);
}
[Fact]
public async Task TryCreateActiveAndTryMarkComplete_ShouldKeepActiveIndexConsistent_WhenRacingOnSameInstanceId()
{
// Arrange
var store = CreateStore();
// Act
await Task.WhenAll(
Enumerable.Range(0, 32).Select(i => Task.Run(() =>
{
if (i % 2 == 0)
{
_ = store.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out _);
return;
}
_ = store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out _);
})));
// Assert
if (store.TryGetActiveForPlayer(PlayerId, out var active))
{
Assert.True(store.TryGet(PlayerId, active.ContractInstanceId, out var row));
Assert.Equal(ContractInstanceStatus.Active, row.Status);
}
if (store.TryGet(PlayerId, InstanceId, out var final) &&
final.Status == ContractInstanceStatus.Completed)
{
Assert.False(store.TryGetActiveForPlayer(PlayerId, out _));
}
}
[Fact]
public async Task TryGetActiveForPlayer_ShouldKeepActiveIndexConsistent_WhenRacingWithCompleteAndReissue()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out _));
// Act
await Task.WhenAll(
Enumerable.Range(0, 32).Select(i => Task.Run(() =>
{
if (i % 3 == 0)
{
_ = store.TryGetActiveForPlayer(PlayerId, out _);
return;
}
if (i % 3 == 1)
{
_ = store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out _);
return;
}
_ = store.TryCreateActive(
PlayerId,
SecondInstanceId,
TemplateId,
SeedBucket,
IssuedAt.AddDays(1),
out _);
})));
// Assert
if (store.TryGetActiveForPlayer(PlayerId, out var active))
{
Assert.True(store.TryGet(PlayerId, active.ContractInstanceId, out var row));
Assert.Equal(ContractInstanceStatus.Active, row.Status);
}
Assert.False(
store.TryGet(PlayerId, InstanceId, out var first) &&
store.TryGet(PlayerId, SecondInstanceId, out var second) &&
first.Status == ContractInstanceStatus.Active &&
second.Status == ContractInstanceStatus.Active);
}
[Fact]
public void TryClearInstance_ShouldReturnTrue_WhenRowMissing()
{
// Arrange
var store = CreateStore();
// Act
var cleared = store.TryClearInstance(PlayerId, InstanceId);
// Assert
Assert.True(cleared);
Assert.False(store.TryGet(PlayerId, InstanceId, out _));
}
[Fact]
public async Task Host_ShouldResolveContractInstanceStoresFromDi()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
using var scope = factory.Services.CreateScope();
var instanceStore = scope.ServiceProvider.GetRequiredService<IContractInstanceStore>();
var outcomeStore = scope.ServiceProvider.GetRequiredService<IContractOutcomeStore>();
// Act
var created = instanceStore.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out var snapshot);
// Assert
Assert.IsType<InMemoryContractInstanceStore>(instanceStore);
Assert.IsType<InMemoryContractOutcomeStore>(outcomeStore);
Assert.True(created);
Assert.Equal(TemplateId, snapshot.TemplateId);
}
private static InMemoryContractInstanceStore CreateStore()
{
var options = Options.Create(new GamePositionOptions { DevPlayerId = PlayerId });
return new InMemoryContractInstanceStore(options);
}
}