using NeonSprawl.Server.Game.Factions; namespace NeonSprawl.Server.Tests.Game.Factions; public class FactionDefinitionCatalogLoaderTests { private const string ValidPrototypeCatalogJson = """ { "schemaVersion": 1, "factions": [ { "id": "prototype_faction_grid_operators", "displayName": "Grid Operators", "minStanding": -100, "maxStanding": 100, "neutralStanding": 0 }, { "id": "prototype_faction_rust_collective", "displayName": "Rust Collective", "minStanding": -100, "maxStanding": 100, "neutralStanding": 0 } ] } """; private static (string Root, string FactionsDir, string SchemaPath) CreateTempContentLayout() { var root = Directory.CreateTempSubdirectory("neon-sprawl-factioncat-"); var factionsDir = Path.Combine(root.FullName, "content", "factions"); var schemaDir = Path.Combine(root.FullName, "content", "schemas"); Directory.CreateDirectory(factionsDir); Directory.CreateDirectory(schemaDir); var schemaPath = Path.Combine(schemaDir, "faction-def.schema.json"); File.Copy(FactionCatalogTestPaths.DiscoverRepoFactionDefSchemaPath(), schemaPath, overwrite: true); return (root.FullName, factionsDir, schemaPath); } [Fact] public void Load_ShouldSucceed_WhenCatalogMatchesRepoPrototypeFile() { // Arrange var factionsDir = FactionCatalogTestPaths.DiscoverRepoFactionsDirectory(); var schemaPath = FactionCatalogTestPaths.DiscoverRepoFactionDefSchemaPath(); // Act var catalog = FactionDefinitionCatalogLoader.Load(factionsDir, schemaPath, NullLogger.Instance); // Assert Assert.Equal(PrototypeE7M3FactionCatalogRules.ExpectedFactionIds.Count, catalog.DistinctFactionCount); } [Fact] public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract() { // Arrange var (_, factionsDir, schemaPath) = CreateTempContentLayout(); File.WriteAllText(Path.Combine(factionsDir, "prototype_factions.json"), ValidPrototypeCatalogJson, Encoding.UTF8); // Act var catalog = FactionDefinitionCatalogLoader.Load(factionsDir, schemaPath, NullLogger.Instance); // Assert Assert.Equal(2, catalog.DistinctFactionCount); Assert.Equal(1, catalog.CatalogJsonFileCount); Assert.True(catalog.ById.TryGetValue("prototype_faction_grid_operators", out var grid)); Assert.NotNull(grid); Assert.Equal("Grid Operators", grid!.DisplayName); Assert.Equal(0, grid.NeutralStanding); } [Fact] public void Load_ShouldThrow_WhenFactionsIsNotArray() { // Arrange var (_, factionsDir, schemaPath) = CreateTempContentLayout(); File.WriteAllText(Path.Combine(factionsDir, "bad_factions.json"), """{"schemaVersion": 1, "factions": "nope"}""", Encoding.UTF8); // Act var ex = Record.Exception(() => FactionDefinitionCatalogLoader.Load(factionsDir, schemaPath, NullLogger.Instance)); // Assert var ioe = Assert.IsType(ex); Assert.Contains("bad_factions.json", ioe.Message, StringComparison.Ordinal); Assert.Contains("expected top-level 'factions' array", ioe.Message, StringComparison.Ordinal); } [Fact] public void Load_ShouldThrow_WhenDuplicateIdAcrossFiles() { // Arrange var (_, factionsDir, schemaPath) = CreateTempContentLayout(); const string singleFaction = """ { "schemaVersion": 1, "factions": [ { "id": "prototype_faction_grid_operators", "displayName": "Grid Operators", "minStanding": -100, "maxStanding": 100, "neutralStanding": 0 } ] } """; File.WriteAllText(Path.Combine(factionsDir, "a_factions.json"), singleFaction, Encoding.UTF8); File.WriteAllText(Path.Combine(factionsDir, "b_factions.json"), singleFaction, Encoding.UTF8); // Act var ex = Record.Exception(() => FactionDefinitionCatalogLoader.Load(factionsDir, schemaPath, NullLogger.Instance)); // Assert var ioe = Assert.IsType(ex); Assert.Contains("duplicate faction id", ioe.Message, StringComparison.Ordinal); } [Fact] public void Load_ShouldThrow_WhenRosterGateFails() { // Arrange var (_, factionsDir, schemaPath) = CreateTempContentLayout(); const string oneFaction = """ { "schemaVersion": 1, "factions": [ { "id": "prototype_faction_grid_operators", "displayName": "Grid Operators", "minStanding": -100, "maxStanding": 100, "neutralStanding": 0 } ] } """; File.WriteAllText(Path.Combine(factionsDir, "prototype_factions.json"), oneFaction, Encoding.UTF8); // Act var ex = Record.Exception(() => FactionDefinitionCatalogLoader.Load(factionsDir, schemaPath, NullLogger.Instance)); // Assert var ioe = Assert.IsType(ex); Assert.Contains("prototype E7M3 expects exactly faction ids", ioe.Message, StringComparison.Ordinal); } [Fact] public void Load_ShouldThrow_WhenFreezeTableDiverges() { // Arrange var (_, factionsDir, schemaPath) = CreateTempContentLayout(); var badDisplay = ValidPrototypeCatalogJson.Replace("Grid Operators", "Wrong Name", StringComparison.Ordinal); File.WriteAllText(Path.Combine(factionsDir, "prototype_factions.json"), badDisplay, Encoding.UTF8); // Act var ex = Record.Exception(() => FactionDefinitionCatalogLoader.Load(factionsDir, schemaPath, NullLogger.Instance)); // Assert var ioe = Assert.IsType(ex); Assert.Contains("must match E7M3 freeze table", ioe.Message, StringComparison.Ordinal); } [Fact] public void TryGetStandingBandGateError_ShouldReturnError_WhenOrderingFails() { // Arrange var rows = new Dictionary(StringComparer.Ordinal) { ["prototype_faction_grid_operators"] = new( "prototype_faction_grid_operators", "Grid Operators", 10, 100, 0), }; // Act var error = PrototypeE7M3FactionCatalogRules.TryGetStandingBandGateError(rows); // Assert Assert.NotNull(error); Assert.Contains("requires minStanding <= neutralStanding <= maxStanding", error, StringComparison.Ordinal); } [Fact] public void Load_ShouldThrow_WhenStandingBandOrderingFails() { // Arrange var (_, factionsDir, schemaPath) = CreateTempContentLayout(); var badBand = ValidPrototypeCatalogJson.Replace("\"minStanding\": -100", "\"minStanding\": 10", StringComparison.Ordinal); File.WriteAllText(Path.Combine(factionsDir, "prototype_factions.json"), badBand, Encoding.UTF8); // Act var ex = Record.Exception(() => FactionDefinitionCatalogLoader.Load(factionsDir, schemaPath, NullLogger.Instance)); // Assert var ioe = Assert.IsType(ex); Assert.Contains("requires minStanding <= neutralStanding <= maxStanding", ioe.Message, StringComparison.Ordinal); } [Fact] public void Load_ShouldThrow_WhenFactionsDirectoryMissing() { // Arrange var missingDir = Path.Combine(Path.GetTempPath(), "neon-sprawl-no-factions-" + Guid.NewGuid().ToString("n")); var schemaPath = FactionCatalogTestPaths.DiscoverRepoFactionDefSchemaPath(); // Act var ex = Record.Exception(() => FactionDefinitionCatalogLoader.Load(missingDir, schemaPath, NullLogger.Instance)); // Assert var ioe = Assert.IsType(ex); Assert.Contains("missing directory", ioe.Message, StringComparison.Ordinal); Assert.Contains(missingDir, ioe.Message, StringComparison.Ordinal); } [Fact] public void Load_ShouldThrow_WhenSchemaFileMissing() { // Arrange var (_, factionsDir, schemaPath) = CreateTempContentLayout(); File.Delete(schemaPath); // Act var ex = Record.Exception(() => FactionDefinitionCatalogLoader.Load(factionsDir, schemaPath, NullLogger.Instance)); // Assert var ioe = Assert.IsType(ex); Assert.Contains("missing schema file", ioe.Message, StringComparison.Ordinal); } }