neon-sprawl/server/NeonSprawl.Server.Tests/Game/Rewards/InMemoryRewardDeliveryStore...

181 lines
6.5 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Contracts;
using NeonSprawl.Server.Game.Rewards;
using NeonSprawl.Server.Tests;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Rewards;
public sealed class InMemoryRewardDeliveryStoreTests
{
private const string PlayerId = "dev-local-1";
private const string QuestId = "prototype_quest_operator_chain";
private static readonly DateTimeOffset DeliveredAt = new(2026, 6, 7, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void TryRecord_ShouldBeIdempotent_AtStoreLayer()
{
// Arrange
var store = new InMemoryRewardDeliveryStore();
var deliveryEvent = CreatePrototypeEvent();
// Act
var first = store.TryRecord(deliveryEvent);
var second = store.TryRecord(deliveryEvent with { DeliveredAt = DeliveredAt.AddHours(1) });
var third = store.TryRecord(
deliveryEvent with
{
PlayerId = "DEV-LOCAL-1",
QuestId = "PROTOTYPE_QUEST_OPERATOR_CHAIN",
});
// Assert
Assert.True(first);
Assert.False(second);
Assert.False(third);
Assert.True(store.TryGet(PlayerId, QuestId, out var stored));
Assert.Equal(deliveryEvent.IdempotencyKey, stored.IdempotencyKey);
Assert.Equal(DeliveredAt, stored.DeliveredAt);
Assert.Single(stored.GrantedItems);
Assert.Single(stored.GrantedSkillXp);
}
[Fact]
public void TryRecord_ShouldFailClosed_WhenPlayerIdIsEmpty()
{
// Arrange
var store = new InMemoryRewardDeliveryStore();
var invalidEvent = CreatePrototypeEvent() with { PlayerId = " " };
// Act
var recorded = store.TryRecord(invalidEvent);
// Assert
Assert.False(recorded);
Assert.False(store.TryGet(" ", QuestId, out _));
}
[Fact]
public void TryRecord_ShouldFailClosed_WhenQuestIdIsEmpty()
{
// Arrange
var store = new InMemoryRewardDeliveryStore();
var invalidEvent = CreatePrototypeEvent() with { QuestId = " " };
// Act
var recorded = store.TryRecord(invalidEvent);
// Assert
Assert.False(recorded);
Assert.False(store.TryGet(PlayerId, " ", out _));
}
[Fact]
public void TryGet_ShouldExposeReadableGrantSummary()
{
// Arrange
var store = new InMemoryRewardDeliveryStore();
var deliveryEvent = CreatePrototypeEvent();
// Act
var recorded = store.TryRecord(deliveryEvent);
var found = store.TryGet(PlayerId, QuestId, out var stored);
// Assert
Assert.True(recorded);
Assert.True(found);
Assert.Equal("survey_drone_kit", stored.GrantedItems[0].ItemId);
Assert.Equal(1, stored.GrantedItems[0].Quantity);
Assert.Equal("salvage", stored.GrantedSkillXp[0].SkillId);
Assert.Equal(50, stored.GrantedSkillXp[0].Amount);
}
[Fact]
public async Task Host_ShouldResolveRewardDeliveryStoreFromDi_WhenStartupSucceeds()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
using var client = factory.CreateClient();
_ = await client.GetAsync("/health");
var deliveryStore = factory.Services.GetRequiredService<IRewardDeliveryStore>();
// Act
var recorded = deliveryStore.TryRecord(CreatePrototypeEvent());
var found = deliveryStore.TryGet(PlayerId, QuestId, out var stored);
// Assert
Assert.IsType<InMemoryRewardDeliveryStore>(deliveryStore);
Assert.True(recorded);
Assert.True(found);
Assert.Equal(RewardDeliveryIds.MakeIdempotencyKey(PlayerId, QuestId), stored.IdempotencyKey);
}
[Fact]
public void TryClear_ShouldRemoveDeliveryRow_WhenPresent()
{
// Arrange
var store = new InMemoryRewardDeliveryStore();
var deliveryEvent = CreatePrototypeEvent();
Assert.True(store.TryRecord(deliveryEvent));
// Act
var cleared = store.TryClear(PlayerId, QuestId);
// Assert
Assert.True(cleared);
Assert.False(store.TryGet(PlayerId, QuestId, out _));
}
[Fact]
public void TryClear_ShouldReturnTrue_WhenRowAlreadyAbsent()
{
// Arrange
var store = new InMemoryRewardDeliveryStore();
// Act
var cleared = store.TryClear(PlayerId, QuestId);
// Assert
Assert.True(cleared);
}
[Fact]
public void TryRecord_ShouldIsolateContractRows_FromQuestRowsWithSameSourceId()
{
// Arrange
var store = new InMemoryRewardDeliveryStore();
const string sharedSourceId = "prototype_shared_source_id";
var questEvent = new RewardDeliveryEvent(
PlayerId,
sharedSourceId,
DeliveredAt,
[new RewardItemGrantApplied("survey_drone_kit", 1)],
[],
[],
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, sharedSourceId));
var contractEvent = new RewardDeliveryEvent(
PlayerId,
QuestId: string.Empty,
DeliveredAt,
[new RewardItemGrantApplied("scrap_metal_bulk", 5)],
[],
[],
ContractOutcomeIds.MakeIdempotencyKey(PlayerId, sharedSourceId),
RewardDeliverySourceKinds.ContractCompletion,
sharedSourceId);
// Act
var questRecorded = store.TryRecord(questEvent);
var contractRecorded = store.TryRecord(contractEvent);
// Assert
Assert.True(questRecorded);
Assert.True(contractRecorded);
Assert.True(store.TryGet(PlayerId, sharedSourceId, out var storedQuest));
Assert.True(
store.TryGet(
PlayerId,
RewardDeliverySourceKinds.ContractCompletion,
sharedSourceId,
out var storedContract));
Assert.Equal(RewardDeliverySourceKinds.QuestCompletion, storedQuest.SourceKind);
Assert.Equal(RewardDeliverySourceKinds.ContractCompletion, storedContract.SourceKind);
Assert.Equal("survey_drone_kit", storedQuest.GrantedItems[0].ItemId);
Assert.Equal("scrap_metal_bulk", storedContract.GrantedItems[0].ItemId);
}
private static RewardDeliveryEvent CreatePrototypeEvent() =>
new(
PlayerId,
QuestId,
DeliveredAt,
[new RewardItemGrantApplied("survey_drone_kit", 1)],
[new RewardSkillXpGrantApplied("salvage", 50)],
[],
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, QuestId));
}