neon-sprawl/server/NeonSprawl.Server.Tests/Game/Factions/ReputationOperationsTests.cs

356 lines
13 KiB
C#

using NeonSprawl.Server.Game.Factions;
using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Tests.Game.Factions;
public sealed class ReputationOperationsTests
{
private const string PlayerId = "dev-local-1";
private const string UnknownPlayerId = "unknown-player-xyz";
private const string GridFactionId = "prototype_faction_grid_operators";
private const string RustFactionId = "prototype_faction_rust_collective";
private const string QuestSourceId = "prototype_quest_operator_chain";
private static readonly DateTimeOffset RecordedAt = new(2026, 6, 15, 14, 0, 0, TimeSpan.Zero);
[Fact]
public void TryApplyDelta_ShouldApplyStandingAndAppendAudit_WhenHappyPath()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
var timeProvider = new FakeTimeProvider(RecordedAt);
// Act
var outcome = ReputationOperations.TryApplyDelta(
PlayerId,
GridFactionId,
15,
"delta-001",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
timeProvider);
// Assert
Assert.True(outcome.Success);
Assert.Null(outcome.ReasonCode);
Assert.Equal(0, outcome.PreviousStanding);
Assert.Equal(15, outcome.NewStanding);
Assert.NotNull(outcome.AuditRow);
Assert.Equal("delta-001", outcome.AuditRow!.Id);
Assert.Equal(15, outcome.AuditRow.DeltaAmount);
Assert.Equal(15, outcome.AuditRow.ResultingStanding);
Assert.Equal(ReputationDeltaSourceKinds.QuestCompletion, outcome.AuditRow.SourceKind);
Assert.Equal(QuestSourceId, outcome.AuditRow.SourceId);
Assert.Equal(RecordedAt, outcome.AuditRow.RecordedAt);
var rows = auditStore.GetDeltasForPlayer(PlayerId);
var read = Assert.Single(rows);
Assert.Equal(outcome.AuditRow, read);
Assert.Equal(15, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
}
[Fact]
public void TryApplyDelta_ShouldClampToMax_WhenDeltaExceedsBand()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
Assert.True(standingStore.TryApplyStandingDelta(PlayerId, GridFactionId, 95).Success);
// Act
var outcome = ReputationOperations.TryApplyDelta(
PlayerId,
GridFactionId,
50,
"delta-max",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.True(outcome.Success);
Assert.Equal(95, outcome.PreviousStanding);
Assert.Equal(100, outcome.NewStanding);
Assert.Equal(5, outcome.AuditRow!.DeltaAmount);
Assert.Equal(100, outcome.AuditRow.ResultingStanding);
Assert.Equal(100, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
}
[Fact]
public void TryApplyDelta_ShouldClampToMin_WhenDeltaBelowBand()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
Assert.True(standingStore.TryApplyStandingDelta(PlayerId, GridFactionId, -95).Success);
// Act
var outcome = ReputationOperations.TryApplyDelta(
PlayerId,
GridFactionId,
-50,
"delta-min",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.True(outcome.Success);
Assert.Equal(-95, outcome.PreviousStanding);
Assert.Equal(-100, outcome.NewStanding);
Assert.Equal(-5, outcome.AuditRow!.DeltaAmount);
Assert.Equal(-100, outcome.AuditRow.ResultingStanding);
Assert.Equal(-100, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
}
[Fact]
public void TryApplyDelta_ShouldDenyUnknownFaction_WithStructuredReason()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
// Act
var outcome = ReputationOperations.TryApplyDelta(
PlayerId,
"not_a_real_faction",
10,
"delta-unknown",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.False(outcome.Success);
Assert.Equal(FactionStandingReasonCodes.UnknownFaction, outcome.ReasonCode);
Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
Assert.Empty(auditStore.GetDeltasForPlayer(PlayerId));
}
[Fact]
public void TryApplyDelta_ShouldDenyInvalidDelta_WhenAmountZero()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
// Act
var outcome = ReputationOperations.TryApplyDelta(
PlayerId,
GridFactionId,
0,
"delta-zero",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.False(outcome.Success);
Assert.Equal(ReputationApplyReasonCodes.InvalidDelta, outcome.ReasonCode);
Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
Assert.Empty(auditStore.GetDeltasForPlayer(PlayerId));
}
[Fact]
public void TryApplyDelta_ShouldDenyInvalidDeltaId_WhenEmpty()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
// Act
var outcome = ReputationOperations.TryApplyDelta(
PlayerId,
GridFactionId,
10,
" ",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.False(outcome.Success);
Assert.Equal(ReputationApplyReasonCodes.InvalidDeltaId, outcome.ReasonCode);
Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
Assert.Empty(auditStore.GetDeltasForPlayer(PlayerId));
}
[Fact]
public void TryApplyDelta_ShouldDenyInvalidSource_WhenEmpty()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
// Act
var emptyKind = ReputationOperations.TryApplyDelta(
PlayerId,
GridFactionId,
10,
"delta-source-kind",
" ",
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
var emptyId = ReputationOperations.TryApplyDelta(
PlayerId,
RustFactionId,
10,
"delta-source-id",
ReputationDeltaSourceKinds.QuestCompletion,
"",
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.False(emptyKind.Success);
Assert.Equal(ReputationApplyReasonCodes.InvalidSource, emptyKind.ReasonCode);
Assert.False(emptyId.Success);
Assert.Equal(ReputationApplyReasonCodes.InvalidSource, emptyId.ReasonCode);
Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
Assert.Equal(0, standingStore.TryGetStanding(PlayerId, RustFactionId).Standing);
Assert.Empty(auditStore.GetDeltasForPlayer(PlayerId));
}
[Fact]
public void TryApplyDelta_ShouldRollbackStanding_WhenAuditAppendFails()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
Assert.True(auditStore.TryAppend(CreateAuditRow("delta-conflict", 1, 1)));
// Act
var outcome = ReputationOperations.TryApplyDelta(
PlayerId,
GridFactionId,
15,
"delta-conflict",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.False(outcome.Success);
Assert.Equal(ReputationApplyReasonCodes.AuditAppendFailed, outcome.ReasonCode);
Assert.Equal(0, outcome.PreviousStanding);
Assert.Equal(0, outcome.NewStanding);
Assert.Null(outcome.AuditRow);
Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
Assert.Single(auditStore.GetDeltasForPlayer(PlayerId));
}
[Fact]
public void TryApplyDelta_ShouldRestoreClampedStanding_WhenAuditAppendFails()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
Assert.True(standingStore.TryApplyStandingDelta(PlayerId, GridFactionId, 95).Success);
Assert.True(auditStore.TryAppend(CreateAuditRow("delta-clamp-conflict", 1, 1)));
// Act
var outcome = ReputationOperations.TryApplyDelta(
PlayerId,
GridFactionId,
50,
"delta-clamp-conflict",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.False(outcome.Success);
Assert.Equal(ReputationApplyReasonCodes.AuditAppendFailed, outcome.ReasonCode);
Assert.Equal(95, outcome.PreviousStanding);
Assert.Equal(95, outcome.NewStanding);
Assert.Null(outcome.AuditRow);
Assert.Equal(95, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
Assert.Single(auditStore.GetDeltasForPlayer(PlayerId));
}
[Fact]
public void TryApplyDelta_ShouldDenyNonWritablePlayer()
{
// Arrange
var standingStore = CreateStandingStore();
var auditStore = CreateAuditStore();
// Act
var outcome = ReputationOperations.TryApplyDelta(
UnknownPlayerId,
GridFactionId,
10,
"delta-player",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.False(outcome.Success);
Assert.Equal(FactionStandingReasonCodes.PlayerNotWritable, outcome.ReasonCode);
Assert.Empty(auditStore.GetDeltasForPlayer(UnknownPlayerId));
}
[Fact]
public async Task Host_ShouldApplyReputationDeltaViaOps_WhenStartupSucceeds()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
using var client = factory.CreateClient();
_ = await client.GetAsync("/health");
var standingStore = factory.Services.GetRequiredService<IFactionStandingStore>();
var auditStore = factory.Services.GetRequiredService<IReputationDeltaStore>();
// Act
var outcome = ReputationOperations.TryApplyDelta(
PlayerId,
GridFactionId,
15,
"delta-host",
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
standingStore,
auditStore,
TimeProvider.System);
// Assert
Assert.True(outcome.Success);
Assert.Equal(15, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
Assert.Single(auditStore.GetDeltasForPlayer(PlayerId));
}
private static InMemoryFactionStandingStore CreateStandingStore()
{
var registry = CreatePrototypeRegistry();
var options = Options.Create(new GamePositionOptions { DevPlayerId = PlayerId });
return new InMemoryFactionStandingStore(options, registry);
}
private static InMemoryReputationDeltaStore CreateAuditStore() => new();
private static ReputationDeltaRow CreateAuditRow(string id, int delta, int resulting) =>
new(
id,
PlayerId,
GridFactionId,
delta,
resulting,
ReputationDeltaSourceKinds.QuestCompletion,
QuestSourceId,
RecordedAt);
private static FactionDefinitionRegistry CreatePrototypeRegistry()
{
var byId = new Dictionary<string, FactionDefRow>(StringComparer.Ordinal)
{
[GridFactionId] = PrototypeE7M3FactionCatalogRules.ExpectedFactionFreeze[GridFactionId],
[RustFactionId] = PrototypeE7M3FactionCatalogRules.ExpectedFactionFreeze[RustFactionId],
};
var catalog = new FactionDefinitionCatalog("/tmp/catalog", byId, catalogJsonFileCount: 1);
return new FactionDefinitionRegistry(catalog);
}
private sealed class FakeTimeProvider(DateTimeOffset utcNow) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => utcNow;
}
}