160 lines
6.0 KiB
C#
160 lines
6.0 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
using NeonSprawl.Server.Game.Items;
|
|
using NeonSprawl.Server.Game.PositionState;
|
|
using NeonSprawl.Server.Tests.Game.PositionState;
|
|
using Npgsql;
|
|
using Xunit;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.Items;
|
|
|
|
[Collection("Postgres integration")]
|
|
public sealed class PlayerInventoryPersistenceIntegrationTests(PostgresIntegrationHarness harness)
|
|
{
|
|
private PostgresWebApplicationFactory Factory => harness.Factory;
|
|
|
|
[RequirePostgresFact]
|
|
public async Task TryAddStackAcrossNewFactory_ShouldPersistSnapshot()
|
|
{
|
|
// Arrange
|
|
await ResetInventoryTableAsync();
|
|
PlayerInventoryMutationOutcome addOutcome;
|
|
|
|
// Act — write through first host
|
|
using (var firstScope = Factory.Services.CreateScope())
|
|
{
|
|
var registry = firstScope.ServiceProvider.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = firstScope.ServiceProvider.GetRequiredService<IPlayerInventoryStore>();
|
|
addOutcome = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"scrap_metal_bulk",
|
|
quantity: 25,
|
|
registry,
|
|
store);
|
|
}
|
|
|
|
// Act — read back through a fresh host
|
|
await using var secondFactory = new PostgresWebApplicationFactory();
|
|
using var secondScope = secondFactory.Services.CreateScope();
|
|
var readStore = secondScope.ServiceProvider.GetRequiredService<IPlayerInventoryStore>();
|
|
var readBack = readStore.TryGetSnapshot("dev-local-1", out var snapshot);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Applied, addOutcome.Kind);
|
|
Assert.True(readBack);
|
|
Assert.NotNull(snapshot);
|
|
Assert.Equal("scrap_metal_bulk", snapshot!.BagSlots[0].ItemId);
|
|
Assert.Equal(25, snapshot.BagSlots[0].Quantity);
|
|
}
|
|
|
|
[RequirePostgresFact]
|
|
public async Task TryAddStack_WhenDenied_ShouldNotPersistPartialState()
|
|
{
|
|
// Arrange
|
|
await ResetInventoryTableAsync();
|
|
using (var seedScope = Factory.Services.CreateScope())
|
|
{
|
|
var registry = seedScope.ServiceProvider.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = seedScope.ServiceProvider.GetRequiredService<IPlayerInventoryStore>();
|
|
for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
|
|
{
|
|
_ = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"survey_drone_kit",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
}
|
|
}
|
|
|
|
// Act
|
|
PlayerInventoryMutationOutcome denyOutcome;
|
|
using (var scope = Factory.Services.CreateScope())
|
|
{
|
|
var registry = scope.ServiceProvider.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = scope.ServiceProvider.GetRequiredService<IPlayerInventoryStore>();
|
|
denyOutcome = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"scrap_metal_bulk",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
}
|
|
|
|
// Act — verify on fresh host
|
|
await using var secondFactory = new PostgresWebApplicationFactory();
|
|
using var secondScope = secondFactory.Services.CreateScope();
|
|
var readStore = secondScope.ServiceProvider.GetRequiredService<IPlayerInventoryStore>();
|
|
readStore.TryGetSnapshot("dev-local-1", out var persisted);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Denied, denyOutcome.Kind);
|
|
Assert.Equal(PlayerInventoryReasonCodes.InventoryFull, denyOutcome.ReasonCode);
|
|
Assert.Equal(PlayerInventorySnapshot.BagSlotCount, OccupiedBagSlotCount(persisted!));
|
|
}
|
|
|
|
private async Task ResetInventoryTableAsync()
|
|
{
|
|
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 inventoryDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V005__player_inventory.sql");
|
|
if (!File.Exists(positionDdlPath))
|
|
{
|
|
throw new FileNotFoundException($"Test DDL not found at '{positionDdlPath}'.", positionDdlPath);
|
|
}
|
|
|
|
if (!File.Exists(inventoryDdlPath))
|
|
{
|
|
throw new FileNotFoundException($"Test DDL not found at '{inventoryDdlPath}'.", inventoryDdlPath);
|
|
}
|
|
|
|
var positionDdl = await File.ReadAllTextAsync(positionDdlPath);
|
|
var inventoryDdl = await File.ReadAllTextAsync(inventoryDdlPath);
|
|
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 applyInventory = new NpgsqlCommand(inventoryDdl, conn))
|
|
{
|
|
await applyInventory.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
await using (var truncateInventory = new NpgsqlCommand("TRUNCATE player_inventory;", conn))
|
|
{
|
|
await truncateInventory.ExecuteNonQueryAsync();
|
|
}
|
|
}
|
|
|
|
private static int OccupiedBagSlotCount(PlayerInventorySnapshot snapshot)
|
|
{
|
|
var count = 0;
|
|
foreach (var slot in snapshot.BagSlots)
|
|
{
|
|
if (!slot.IsEmpty)
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
}
|