259 lines
8.2 KiB
C#
259 lines
8.2 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
using NeonSprawl.Server.Game.Contracts;
|
|
using NeonSprawl.Server.Game.PositionState;
|
|
using NeonSprawl.Server.Tests;
|
|
using Xunit;
|
|
|
|
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 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);
|
|
}
|
|
}
|