178 lines
6.4 KiB
C#
178 lines
6.4 KiB
C#
using NeonSprawl.Server.Game.Contracts;
|
|
using NeonSprawl.Server.Game.Rewards;
|
|
|
|
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));
|
|
}
|