neon-sprawl/server/NeonSprawl.Server.Tests/Game/Quests/InMemoryPlayerQuestStateSto...

167 lines
6.1 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Quests;
using NeonSprawl.Server.Tests;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Quests;
public sealed class InMemoryPlayerQuestStateStoreTests
{
private const string PlayerId = "dev-local-1";
private const string UnknownPlayerId = "unknown-player-xyz";
private const string GatherQuestId = "prototype_quest_gather_intro";
private const string ChainQuestId = PrototypeE7M1QuestCatalogRules.ChainQuestId;
private const string GatherObjectiveId = "gather_intro_obj_scrap";
private static readonly DateTimeOffset CompletedAt = new(2026, 6, 3, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void TryGetProgress_ShouldReturnFalse_WhenRowMissing()
{
// Arrange
var store = CreateStore();
// Act
var found = store.TryGetProgress(PlayerId, GatherQuestId, out _);
// Assert
Assert.False(found);
}
[Fact]
public void TryActivate_ShouldCreateActiveRowAtStepZero()
{
// Arrange
var store = CreateStore();
// Act
var activated = store.TryActivate(PlayerId, GatherQuestId, out var snapshot);
// Assert
Assert.True(activated);
Assert.Equal(QuestProgressStatus.Active, snapshot.Status);
Assert.Equal(0, snapshot.CurrentStepIndex);
Assert.Empty(snapshot.ObjectiveCounters);
Assert.Null(snapshot.CompletedAt);
}
[Fact]
public void TryActivate_ShouldReturnFalse_WhenAlreadyActive()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _));
// Act
var second = store.TryActivate(PlayerId, GatherQuestId, out var snapshot);
// Assert
Assert.False(second);
Assert.Equal(QuestProgressStatus.Active, snapshot.Status);
}
[Fact]
public void TryUpdateObjectiveCounter_ShouldUpdateMap_WhenActive()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _));
// Act
var updated = store.TryUpdateObjectiveCounter(PlayerId, GatherQuestId, GatherObjectiveId, 3, out var snapshot);
// Assert
Assert.True(updated);
Assert.Equal(3, snapshot.ObjectiveCounters[GatherObjectiveId]);
}
[Fact]
public void TryAdvanceStep_ShouldClearCountersAndBumpIndex()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryActivate(PlayerId, ChainQuestId, out _));
Assert.True(store.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, "chain_obj_gather", 5, out _));
// Act
var advanced = store.TryAdvanceStep(PlayerId, ChainQuestId, 1, out var snapshot);
// Assert
Assert.True(advanced);
Assert.Equal(1, snapshot.CurrentStepIndex);
Assert.Empty(snapshot.ObjectiveCounters);
}
[Fact]
public void TryAdvanceStep_ShouldReturnFalse_WhenIndexNotIncreasing()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _));
// Act
var sameIndex = store.TryAdvanceStep(PlayerId, GatherQuestId, 0, out _);
// Assert
Assert.False(sameIndex);
}
[Fact]
public void TryMarkComplete_ShouldBeIdempotent()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _));
// Act
var first = store.TryMarkComplete(PlayerId, GatherQuestId, CompletedAt, out var firstSnapshot);
var second = store.TryMarkComplete(PlayerId, GatherQuestId, CompletedAt.AddHours(1), out var secondSnapshot);
// Assert
Assert.True(first);
Assert.False(second);
Assert.Equal(QuestProgressStatus.Completed, secondSnapshot.Status);
Assert.Equal(CompletedAt, secondSnapshot.CompletedAt);
Assert.Equal(firstSnapshot.CurrentStepIndex, secondSnapshot.CurrentStepIndex);
}
[Fact]
public void TryMarkComplete_ShouldDenyRegression_OnActivateAdvanceAndCounter()
{
// Arrange
var store = CreateStore();
Assert.True(store.TryActivate(PlayerId, GatherQuestId, out _));
Assert.True(store.TryMarkComplete(PlayerId, GatherQuestId, CompletedAt, out _));
// Act
var reactivate = store.TryActivate(PlayerId, GatherQuestId, out _);
var advance = store.TryAdvanceStep(PlayerId, GatherQuestId, 1, out _);
var counter = store.TryUpdateObjectiveCounter(PlayerId, GatherQuestId, GatherObjectiveId, 1, out _);
// Assert
Assert.False(reactivate);
Assert.False(advance);
Assert.False(counter);
Assert.True(store.TryGetProgress(PlayerId, GatherQuestId, out var snapshot));
Assert.Equal(QuestProgressStatus.Completed, snapshot.Status);
}
[Fact]
public void TryActivate_ShouldReturnFalse_ForUnknownPlayer()
{
// Arrange
var store = CreateStore();
// Act
var activated = store.TryActivate(UnknownPlayerId, GatherQuestId, out _);
// Assert
Assert.False(activated);
}
[Fact]
public async Task Host_ShouldResolveStore_AndActivateGatherIntro()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
using var scope = factory.Services.CreateScope();
var store = scope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
// Act
var activated = store.TryActivate(PlayerId, GatherQuestId, out var snapshot);
var found = store.TryGetProgress(PlayerId, GatherQuestId, out var readBack);
// Assert
Assert.True(activated);
Assert.True(found);
Assert.Equal(snapshot.QuestId, readBack.QuestId);
Assert.Equal(QuestProgressStatus.Active, readBack.Status);
}
private static InMemoryPlayerQuestStateStore CreateStore()
{
var options = Microsoft.Extensions.Options.Options.Create(new GamePositionOptions { DevPlayerId = PlayerId });
return new InMemoryPlayerQuestStateStore(options);
}
}