neon-sprawl/server/NeonSprawl.Server.Tests/Game/Skills/SkillProgressionGrantPersis...

95 lines
3.8 KiB
C#

using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Skills;
using NeonSprawl.Server.Tests.Game.PositionState;
using Npgsql;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Skills;
[Collection("Postgres integration")]
public sealed class SkillProgressionGrantPersistenceIntegrationTests(PostgresIntegrationHarness harness)
{
private PostgresWebApplicationFactory Factory => harness.Factory;
[RequirePostgresFact]
public async Task PostGrantThenGetAcrossNewFactory_ShouldPersistSkillXp()
{
// Arrange
await ResetProgressionTableAsync();
var grant = new SkillProgressionGrantRequest
{
SchemaVersion = SkillProgressionGrantRequest.CurrentSchemaVersion,
SkillId = "salvage",
Amount = 50,
SourceKind = "activity",
};
HttpResponseMessage postResponse;
// Act — write grant through first host
using var firstClient = Factory.CreateClient();
postResponse = await firstClient.PostAsJsonAsync(
"/game/players/dev-local-1/skill-progression",
grant);
// Act — read back through a fresh host (forces DB round-trip / new DI scope)
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondClient = secondFactory.CreateClient();
var snapshot =
await secondClient.GetFromJsonAsync<SkillProgressionSnapshotResponse>(
"/game/players/dev-local-1/skill-progression");
// Assert
Assert.True(postResponse.IsSuccessStatusCode);
Assert.NotNull(snapshot);
var salvage = Assert.Single(snapshot!.Skills!, static s => s.Id == "salvage");
Assert.Equal(50, salvage.Xp);
Assert.Equal(1, salvage.Level);
}
private async Task ResetProgressionTableAsync()
{
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 progressionDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V003__player_skill_progression.sql");
if (!File.Exists(positionDdlPath))
{
throw new FileNotFoundException($"Test DDL not found at '{positionDdlPath}'.", positionDdlPath);
}
if (!File.Exists(progressionDdlPath))
{
throw new FileNotFoundException($"Test DDL not found at '{progressionDdlPath}'.", progressionDdlPath);
}
var positionDdl = await File.ReadAllTextAsync(positionDdlPath);
var progressionDdl = await File.ReadAllTextAsync(progressionDdlPath);
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 applyProgression = new NpgsqlCommand(progressionDdl, conn);
await applyProgression.ExecuteNonQueryAsync();
await using var truncateProgression = new NpgsqlCommand("TRUNCATE player_skill_progression;", conn);
await truncateProgression.ExecuteNonQueryAsync();
}
}