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

142 lines
4.9 KiB
C#

using Microsoft.Extensions.Options;
using NeonSprawl.Server.Game.Contracts;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Rewards;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Contracts;
public sealed class InMemoryContractOutcomeStoreTests
{
private const string PlayerId = "dev-local-1";
private const string InstanceId = "prototype_contract_instance_001";
private const string OutcomeRowId = "outcome-row-001";
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 RecordedAt = new(2026, 6, 22, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void TryAppend_ShouldPersistAndQueryByInstanceAndIdempotencyKey()
{
// Arrange
var (instanceStore, store) = CreateStores();
SeedActiveInstance(instanceStore);
var row = CreatePrototypeOutcome();
// Act
var appended = store.TryAppend(row);
var foundByKey = store.TryGetByIdempotencyKey(row.IdempotencyKey, out var byKey);
var listed = store.GetOutcomesForInstance(InstanceId);
// Assert
Assert.True(appended);
Assert.True(foundByKey);
Assert.Equal(row.Id, byKey.Id);
Assert.Single(listed);
Assert.Equal("scrap_metal_bulk", listed[0].GrantedItems[0].ItemId);
Assert.Equal(5, listed[0].GrantedItems[0].Quantity);
Assert.Equal("salvage", listed[0].GrantedSkillXp[0].SkillId);
Assert.Equal(15, listed[0].GrantedSkillXp[0].Amount);
}
[Fact]
public void TryAppend_ShouldDenyDuplicateRowId()
{
// Arrange
var (instanceStore, store) = CreateStores();
SeedActiveInstance(instanceStore);
var row = CreatePrototypeOutcome();
Assert.True(store.TryAppend(row));
// Act
var duplicateId = store.TryAppend(row with { IdempotencyKey = "other-key" });
// Assert
Assert.False(duplicateId);
}
[Fact]
public void TryAppend_ShouldDenyDuplicateIdempotencyKey()
{
// Arrange
var (instanceStore, store) = CreateStores();
SeedActiveInstance(instanceStore);
var row = CreatePrototypeOutcome();
Assert.True(store.TryAppend(row));
// Act
var duplicateKey = store.TryAppend(row with { Id = "outcome-row-002" });
// Assert
Assert.False(duplicateKey);
}
[Fact]
public void TryAppend_ShouldFailClosed_WhenRequiredFieldsEmpty()
{
// Arrange
var (instanceStore, store) = CreateStores();
SeedActiveInstance(instanceStore);
var invalidRow = CreatePrototypeOutcome() with { PlayerId = " " };
// Act
var appended = store.TryAppend(invalidRow);
// Assert
Assert.False(appended);
Assert.Empty(store.GetOutcomesForInstance(InstanceId));
}
[Fact]
public void TryAppend_ShouldReturnFalse_WhenContractInstanceMissing()
{
// Arrange
var (_, store) = CreateStores();
var row = CreatePrototypeOutcome();
// Act
var appended = store.TryAppend(row);
// Assert
Assert.False(appended);
Assert.Empty(store.GetOutcomesForInstance(InstanceId));
}
[Fact]
public void TryAppend_ShouldReturnFalse_WhenPlayerIdDoesNotMatchInstanceOwner()
{
// Arrange
var (instanceStore, store) = CreateStores();
SeedActiveInstance(instanceStore);
var row = CreatePrototypeOutcome() with { PlayerId = "other-player-xyz" };
// Act
var appended = store.TryAppend(row);
// Assert
Assert.False(appended);
Assert.Empty(store.GetOutcomesForInstance(InstanceId));
}
private static (InMemoryContractInstanceStore InstanceStore, InMemoryContractOutcomeStore OutcomeStore) CreateStores()
{
var options = Options.Create(new GamePositionOptions { DevPlayerId = PlayerId });
var instanceStore = new InMemoryContractInstanceStore(options);
return (instanceStore, new InMemoryContractOutcomeStore(instanceStore));
}
private static void SeedActiveInstance(InMemoryContractInstanceStore instanceStore)
{
Assert.True(instanceStore.TryCreateActive(
PlayerId,
InstanceId,
TemplateId,
SeedBucket,
IssuedAt,
out _));
}
private static ContractOutcomeRow CreatePrototypeOutcome()
{
var idempotencyKey = ContractOutcomeIds.MakeIdempotencyKey(PlayerId, InstanceId);
return new ContractOutcomeRow(
OutcomeRowId,
InstanceId,
PlayerId,
idempotencyKey,
[new RewardItemGrantApplied("scrap_metal_bulk", 5)],
[new RewardSkillXpGrantApplied("salvage", 15)],
[],
RecordedAt);
}
}