neon-sprawl/server/NeonSprawl.Server.Tests/Game/Factions/ReputationDeltaPersistenceI...

98 lines
3.7 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NeonSprawl.Server.Game.Factions;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Tests.Game.PositionState;
using Npgsql;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Factions;
[Collection("Postgres integration")]
public sealed class ReputationDeltaPersistenceIntegrationTests(PostgresIntegrationHarness harness)
{
private PostgresWebApplicationFactory Factory => harness.Factory;
private const string PlayerId = "dev-local-1";
private const string GridFactionId = "prototype_faction_grid_operators";
private static readonly DateTimeOffset RecordedAt = new(2026, 6, 15, 14, 0, 0, TimeSpan.Zero);
[RequirePostgresFact]
public async Task AppendThenGetAcrossNewFactory_ShouldPersistAuditRow()
{
// Arrange
await ResetReputationDeltaTableAsync();
var row = new ReputationDeltaRow(
"delta-persist-001",
PlayerId,
GridFactionId,
15,
15,
ReputationDeltaSourceKinds.QuestCompletion,
"prototype_quest_operator_chain",
RecordedAt);
// Act — write through first host
using (var firstScope = Factory.Services.CreateScope())
{
var store = firstScope.ServiceProvider.GetRequiredService<IReputationDeltaStore>();
Assert.True(store.TryAppend(row));
}
IReadOnlyList<ReputationDeltaRow> readBack;
await using (var secondFactory = new PostgresWebApplicationFactory())
{
using var secondScope = secondFactory.Services.CreateScope();
var store = secondScope.ServiceProvider.GetRequiredService<IReputationDeltaStore>();
readBack = store.GetDeltasForPlayer(PlayerId);
}
// Assert
var persisted = Assert.Single(readBack);
Assert.Equal(row.Id, persisted.Id);
Assert.Equal(15, persisted.DeltaAmount);
Assert.Equal(15, persisted.ResultingStanding);
Assert.Equal(ReputationDeltaSourceKinds.QuestCompletion, persisted.SourceKind);
}
private async Task ResetReputationDeltaTableAsync()
{
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 auditDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V010__reputation_delta_audit.sql");
if (!File.Exists(positionDdlPath) || !File.Exists(auditDdlPath))
{
throw new FileNotFoundException("Test DDL for reputation delta persistence not found.");
}
var positionDdl = await File.ReadAllTextAsync(positionDdlPath);
var auditDdl = await File.ReadAllTextAsync(auditDdlPath);
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 applyAudit = new NpgsqlCommand(auditDdl, conn))
{
await applyAudit.ExecuteNonQueryAsync();
}
}
}