neon-sprawl/server/NeonSprawl.Server.Tests/Game/Encounters/RewardTableDefinitionCatalo...

302 lines
12 KiB
C#

using NeonSprawl.Server.Game.Encounters;
namespace NeonSprawl.Server.Tests.Game.Encounters;
public class RewardTableDefinitionCatalogLoaderTests
{
private const string ValidPrototypeCatalogJson =
"""
{
"schemaVersion": 1,
"rewardTables": [
{
"id": "prototype_combat_pocket_clear",
"displayName": "Prototype Combat Pocket Clear",
"fixedGrants": [
{ "itemId": "scrap_metal_bulk", "quantity": 10 },
{ "itemId": "contract_handoff_token", "quantity": 1 }
]
}
]
}
""";
private static (string Root, string RewardTablesDir, string TableSchemaPath, string GrantRowSchemaPath) CreateTempContentLayout()
{
var root = Directory.CreateTempSubdirectory("neon-sprawl-rewardtablecat-");
var rewardTablesDir = Path.Combine(root.FullName, "content", "reward-tables");
var schemaDir = Path.Combine(root.FullName, "content", "schemas");
Directory.CreateDirectory(rewardTablesDir);
Directory.CreateDirectory(schemaDir);
var tableSchemaPath = Path.Combine(schemaDir, "reward-table.schema.json");
var grantRowSchemaPath = Path.Combine(schemaDir, "reward-grant-row.schema.json");
File.Copy(EncounterCatalogTestPaths.DiscoverRepoRewardTableDefSchemaPath(), tableSchemaPath, overwrite: true);
File.Copy(EncounterCatalogTestPaths.DiscoverRepoRewardGrantRowSchemaPath(), grantRowSchemaPath, overwrite: true);
return (root.FullName, rewardTablesDir, tableSchemaPath, grantRowSchemaPath);
}
private static void WriteCatalog(string rewardTablesDir, string catalogJson) =>
File.WriteAllText(Path.Combine(rewardTablesDir, "prototype_reward_tables.json"), catalogJson, Encoding.UTF8);
private static RewardTableDefinitionCatalog LoadCatalog(
string rewardTablesDir,
string tableSchemaPath,
string grantRowSchemaPath,
IReadOnlySet<string>? knownItemIds = null) =>
RewardTableDefinitionCatalogLoader.Load(
rewardTablesDir,
tableSchemaPath,
grantRowSchemaPath,
knownItemIds ?? EncounterCatalogTestPaths.Slice1KnownItemIds,
NullLogger.Instance);
[Fact]
public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
{
// Arrange
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
WriteCatalog(rewardTablesDir, ValidPrototypeCatalogJson);
// Act
var catalog = LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath);
// Assert
Assert.Equal(1, catalog.DistinctRewardTableCount);
Assert.Equal(1, catalog.CatalogJsonFileCount);
Assert.True(catalog.TryGetRewardTable("prototype_combat_pocket_clear", out var table));
Assert.NotNull(table);
Assert.Equal("Prototype Combat Pocket Clear", table!.DisplayName);
Assert.Equal(2, table.FixedGrants.Count);
Assert.Equal("scrap_metal_bulk", table.FixedGrants[0].ItemId);
Assert.Equal(10, table.FixedGrants[0].Quantity);
Assert.Equal("contract_handoff_token", table.FixedGrants[1].ItemId);
Assert.Equal(1, table.FixedGrants[1].Quantity);
}
[Fact]
public void Load_ShouldThrow_WhenRewardTablesIsNotArray()
{
// Arrange
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
File.WriteAllText(
Path.Combine(rewardTablesDir, "bad_reward_tables.json"),
"""{"schemaVersion": 1, "rewardTables": "nope"}""",
Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("bad_reward_tables.json", ioe.Message, StringComparison.Ordinal);
Assert.Contains("expected top-level 'rewardTables' array", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenSchemaVersionIsNotOne()
{
// Arrange
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
File.WriteAllText(
Path.Combine(rewardTablesDir, "bad_reward_tables.json"),
"""{"schemaVersion": 2, "rewardTables": []}""",
Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("expected schemaVersion 1", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenDuplicateRewardTableIdAcrossFiles()
{
// Arrange
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
WriteCatalog(rewardTablesDir, ValidPrototypeCatalogJson);
File.WriteAllText(
Path.Combine(rewardTablesDir, "extra_reward_tables.json"),
"""
{
"schemaVersion": 1,
"rewardTables": [
{
"id": "prototype_combat_pocket_clear",
"displayName": "Duplicate Table",
"fixedGrants": [
{ "itemId": "scrap_metal_bulk", "quantity": 10 },
{ "itemId": "contract_handoff_token", "quantity": 1 }
]
}
]
}
""",
Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("duplicate reward table id 'prototype_combat_pocket_clear'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenE5M3AllowlistMismatch()
{
// Arrange
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
File.WriteAllText(
Path.Combine(rewardTablesDir, "prototype_reward_tables.json"),
"""
{
"schemaVersion": 1,
"rewardTables": [
{
"id": "wrong_reward_table_id",
"displayName": "Wrong Table",
"fixedGrants": [
{ "itemId": "scrap_metal_bulk", "quantity": 10 },
{ "itemId": "contract_handoff_token", "quantity": 1 }
]
}
]
}
""",
Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("prototype E5M3 expects exactly reward table ids", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenUnknownItemIdInFixedGrants()
{
// Arrange
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
File.WriteAllText(
Path.Combine(rewardTablesDir, "prototype_reward_tables.json"),
"""
{
"schemaVersion": 1,
"rewardTables": [
{
"id": "prototype_combat_pocket_clear",
"displayName": "Prototype Combat Pocket Clear",
"fixedGrants": [
{ "itemId": "unknown_item_id", "quantity": 1 }
]
}
]
}
""",
Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("itemId 'unknown_item_id' is not in item catalogs", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenDuplicateItemIdInFixedGrants()
{
// Arrange
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
File.WriteAllText(
Path.Combine(rewardTablesDir, "prototype_reward_tables.json"),
"""
{
"schemaVersion": 1,
"rewardTables": [
{
"id": "prototype_combat_pocket_clear",
"displayName": "Prototype Combat Pocket Clear",
"fixedGrants": [
{ "itemId": "scrap_metal_bulk", "quantity": 5 },
{ "itemId": "scrap_metal_bulk", "quantity": 5 }
]
}
]
}
""",
Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("duplicate itemId 'scrap_metal_bulk'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenFrozenGrantQuantitiesMismatch()
{
// Arrange
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
File.WriteAllText(
Path.Combine(rewardTablesDir, "prototype_reward_tables.json"),
"""
{
"schemaVersion": 1,
"rewardTables": [
{
"id": "prototype_combat_pocket_clear",
"displayName": "Prototype Combat Pocket Clear",
"fixedGrants": [
{ "itemId": "scrap_metal_bulk", "quantity": 9 },
{ "itemId": "contract_handoff_token", "quantity": 1 }
]
}
]
}
""",
Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("prototype_combat_pocket_clear fixedGrants must match", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenRowViolatesSchema()
{
// Arrange
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
const string bad = """
{
"schemaVersion": 1,
"rewardTables": [
{
"id": "prototype_combat_pocket_clear",
"displayName": "",
"fixedGrants": [
{ "itemId": "scrap_metal_bulk", "quantity": 10 },
{ "itemId": "contract_handoff_token", "quantity": 1 }
]
}
]
}
""";
File.WriteAllText(Path.Combine(rewardTablesDir, "bad_reward_tables.json"), bad, Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("bad_reward_tables.json", ioe.Message, StringComparison.Ordinal);
Assert.Contains("Reward table catalog validation failed", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenSchemaFileMissing()
{
// Arrange
var (_, rewardTablesDir, _, grantRowSchemaPath) = CreateTempContentLayout();
WriteCatalog(rewardTablesDir, ValidPrototypeCatalogJson);
var missingSchema = Path.Combine(rewardTablesDir, "missing-reward-table.schema.json");
// Act
var ex = Record.Exception(() =>
LoadCatalog(rewardTablesDir, missingSchema, grantRowSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("missing schema file", ioe.Message, StringComparison.Ordinal);
Assert.Contains(missingSchema, ioe.Message, StringComparison.Ordinal);
}
}