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

122 lines
4.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 FactionStandingPersistenceIntegrationTests(PostgresIntegrationHarness harness)
{
private PostgresWebApplicationFactory Factory => harness.Factory;
private const string PlayerId = "dev-local-1";
private const string GridFactionId = "prototype_faction_grid_operators";
[RequirePostgresFact]
public async Task ApplyDeltaThenGetAcrossNewFactory_ShouldPersistStanding()
{
// Arrange
await ResetFactionStandingTableAsync();
// Act — write through first host
using (var firstScope = Factory.Services.CreateScope())
{
var store = firstScope.ServiceProvider.GetRequiredService<IFactionStandingStore>();
var outcome = store.TryApplyStandingDelta(PlayerId, GridFactionId, 15);
Assert.True(outcome.Success);
Assert.Equal(15, outcome.NewStanding);
}
FactionStandingReadOutcome readBack;
await using (var secondFactory = new PostgresWebApplicationFactory())
{
using var secondScope = secondFactory.Services.CreateScope();
var store = secondScope.ServiceProvider.GetRequiredService<IFactionStandingStore>();
readBack = store.TryGetStanding(PlayerId, GridFactionId);
}
// Assert
Assert.True(readBack.Success);
Assert.Equal(15, readBack.Standing);
}
[RequirePostgresFact]
public async Task TryGetStanding_ShouldClampRead_WhenStoredValueExceedsMax()
{
// Arrange
await ResetFactionStandingTableAsync();
var cs = Environment.GetEnvironmentVariable("ConnectionStrings__NeonSprawl")
?? throw new InvalidOperationException("ConnectionStrings__NeonSprawl is not set.");
await using (var conn = new NpgsqlConnection(cs))
{
await conn.OpenAsync();
await using var insert = new NpgsqlCommand(
"""
INSERT INTO player_faction_standing (player_id, faction_id, standing, updated_at)
VALUES (@pid, @fid, @standing, now());
""",
conn);
insert.Parameters.AddWithValue("pid", PlayerId);
insert.Parameters.AddWithValue("fid", GridFactionId);
insert.Parameters.AddWithValue("standing", 999);
await insert.ExecuteNonQueryAsync();
}
// Act
FactionStandingReadOutcome readBack;
using (var scope = Factory.Services.CreateScope())
{
var store = scope.ServiceProvider.GetRequiredService<IFactionStandingStore>();
readBack = store.TryGetStanding(PlayerId, GridFactionId);
}
// Assert
Assert.True(readBack.Success);
Assert.Equal(100, readBack.Standing);
}
private async Task ResetFactionStandingTableAsync()
{
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 standingDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V009__player_faction_standing.sql");
if (!File.Exists(positionDdlPath) || !File.Exists(standingDdlPath))
{
throw new FileNotFoundException("Test DDL for faction standing persistence not found.");
}
var positionDdl = await File.ReadAllTextAsync(positionDdlPath);
var standingDdl = await File.ReadAllTextAsync(standingDdlPath);
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 applyStanding = new NpgsqlCommand(standingDdl, conn))
{
await applyStanding.ExecuteNonQueryAsync();
}
}
}