148 lines
6.2 KiB
C#
148 lines
6.2 KiB
C#
using NeonSprawl.Server.Game.PositionState;
|
|
using NeonSprawl.Server.Game.Quests;
|
|
using NeonSprawl.Server.Tests.Game.PositionState;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.Quests;
|
|
|
|
[Collection("Postgres integration")]
|
|
public sealed class PlayerQuestProgressPersistenceIntegrationTests(PostgresIntegrationHarness harness)
|
|
{
|
|
private PostgresWebApplicationFactory Factory => harness.Factory;
|
|
|
|
private const string PlayerId = "dev-local-1";
|
|
private static readonly DateTimeOffset CompletedAt = new(2026, 6, 3, 14, 30, 0, TimeSpan.Zero);
|
|
|
|
[RequirePostgresFact]
|
|
public async Task TryActivate_ShouldReturnOneTrueAndRestFalse_WhenCalledConcurrently()
|
|
{
|
|
// Arrange
|
|
await ResetQuestProgressTableAsync();
|
|
const int concurrentCalls = 8;
|
|
const string questId = PrototypeE7M1QuestCatalogRules.GatherIntroQuestId;
|
|
|
|
// Act
|
|
var results = await Task.WhenAll(
|
|
Enumerable.Range(0, concurrentCalls).Select(_ => Task.Run(() =>
|
|
{
|
|
using var scope = Factory.Services.CreateScope();
|
|
var store = scope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
|
|
return store.TryActivate(PlayerId, questId, out QuestStepState _);
|
|
})));
|
|
|
|
// Assert
|
|
Assert.Equal(1, results.Count(static r => r));
|
|
Assert.Equal(concurrentCalls - 1, results.Count(static r => !r));
|
|
Assert.True(
|
|
Factory.Services.CreateScope().ServiceProvider
|
|
.GetRequiredService<IPlayerQuestStateStore>()
|
|
.TryGetProgress(PlayerId, questId, out var snapshot));
|
|
Assert.Equal(QuestProgressStatus.Active, snapshot.Status);
|
|
Assert.Equal(0, snapshot.CurrentStepIndex);
|
|
}
|
|
|
|
[RequirePostgresFact]
|
|
public async Task ActivateCounterComplete_ShouldPersistAcrossNewFactory()
|
|
{
|
|
// Arrange
|
|
await ResetQuestProgressTableAsync();
|
|
|
|
// Act — write through first host
|
|
using (var firstScope = Factory.Services.CreateScope())
|
|
{
|
|
var store = firstScope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
|
|
Assert.True(store.TryActivate(PlayerId, PrototypeE7M1QuestCatalogRules.GatherIntroQuestId, out _));
|
|
Assert.True(store.TryUpdateObjectiveCounter(
|
|
PlayerId,
|
|
PrototypeE7M1QuestCatalogRules.GatherIntroQuestId,
|
|
PrototypeE7M1QuestCatalogRules.GatherIntroFirstObjectiveId,
|
|
3,
|
|
out _));
|
|
Assert.True(store.TryMarkComplete(
|
|
PlayerId,
|
|
PrototypeE7M1QuestCatalogRules.GatherIntroQuestId,
|
|
CompletedAt,
|
|
out _));
|
|
}
|
|
|
|
// Act — read back through a fresh host
|
|
QuestStepState readBack;
|
|
await using var secondFactory = new PostgresWebApplicationFactory();
|
|
using var secondScope = secondFactory.Services.CreateScope();
|
|
var readStore = secondScope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
|
|
readBack = readStore.TryGetProgress(
|
|
PlayerId,
|
|
PrototypeE7M1QuestCatalogRules.GatherIntroQuestId,
|
|
out var snapshot)
|
|
? snapshot
|
|
: null!;
|
|
|
|
// Assert
|
|
Assert.NotNull(readBack);
|
|
Assert.Equal(QuestProgressStatus.Completed, readBack.Status);
|
|
Assert.Equal(0, readBack.CurrentStepIndex);
|
|
Assert.Equal(3, readBack.ObjectiveCounters[PrototypeE7M1QuestCatalogRules.GatherIntroFirstObjectiveId]);
|
|
Assert.Equal(CompletedAt, readBack.CompletedAt);
|
|
}
|
|
|
|
[RequirePostgresFact]
|
|
public async Task TryClearProgress_ShouldRemoveRowAndPersistAcrossNewFactory()
|
|
{
|
|
// Arrange
|
|
await ResetQuestProgressTableAsync();
|
|
const string questId = PrototypeE7M1QuestCatalogRules.GatherIntroQuestId;
|
|
|
|
// Act
|
|
using (var firstScope = Factory.Services.CreateScope())
|
|
{
|
|
var store = firstScope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
|
|
Assert.True(store.TryActivate(PlayerId, questId, out _));
|
|
Assert.True(store.TryMarkComplete(PlayerId, questId, CompletedAt, out _));
|
|
Assert.True(store.TryClearProgress(PlayerId, questId));
|
|
Assert.False(store.TryGetProgress(PlayerId, questId, out _));
|
|
}
|
|
|
|
bool readBackExists;
|
|
await using var secondFactory = new PostgresWebApplicationFactory();
|
|
using var secondScope = secondFactory.Services.CreateScope();
|
|
var readStore = secondScope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
|
|
readBackExists = readStore.TryGetProgress(PlayerId, questId, out _);
|
|
|
|
// Assert
|
|
Assert.False(readBackExists);
|
|
}
|
|
|
|
private async Task ResetQuestProgressTableAsync()
|
|
{
|
|
var cs = Environment.GetEnvironmentVariable("ConnectionStrings__NeonSprawl");
|
|
if (string.IsNullOrWhiteSpace(cs))
|
|
{
|
|
throw new InvalidOperationException("ConnectionStrings__NeonSprawl is not set.");
|
|
}
|
|
|
|
_ = Factory.Services;
|
|
var options = Factory.Services.GetRequiredService<IOptions<GamePositionOptions>>().Value;
|
|
|
|
var positionDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V001__player_position.sql");
|
|
var questDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V008__player_quest_progress.sql");
|
|
if (!File.Exists(positionDdlPath) || !File.Exists(questDdlPath))
|
|
{
|
|
throw new FileNotFoundException("Test DDL for quest progress persistence not found.");
|
|
}
|
|
|
|
var positionDdl = await File.ReadAllTextAsync(positionDdlPath);
|
|
var questDdl = await File.ReadAllTextAsync(questDdlPath);
|
|
await using var conn = new NpgsqlConnection(cs);
|
|
await conn.OpenAsync();
|
|
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
|
|
await applyPosition.ExecuteNonQueryAsync();
|
|
|
|
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
|
|
await truncate.ExecuteNonQueryAsync();
|
|
|
|
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
|
|
|
|
await using var applyQuest = new NpgsqlCommand(questDdl, conn);
|
|
await applyQuest.ExecuteNonQueryAsync();
|
|
}
|
|
}
|