neon-sprawl/server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionCatalogLoade...

627 lines
28 KiB
C#

using System.Collections.Frozen;
using System.Net;
using System.Text;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NeonSprawl.Server.Game.Crafting;
using NeonSprawl.Server.Game.Encounters;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Quests;
using NeonSprawl.Server.Tests;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Quests;
public class QuestDefinitionCatalogLoaderTests
{
private static readonly FrozenSet<string> KnownItemIds = PrototypeSlice1ItemCatalogRules.ExpectedItemIds;
private static readonly FrozenSet<string> KnownRecipeIds = PrototypeSlice3RecipeCatalogRules.ExpectedRecipeIds;
private static readonly FrozenSet<string> KnownEncounterIds = PrototypeE5M3EncounterCatalogRules.ExpectedEncounterIds;
private const string ValidPrototypeCatalogJson =
"""
{
"schemaVersion": 1,
"quests": [
{
"id": "prototype_quest_gather_intro",
"displayName": "Intro: Salvage Run",
"prerequisiteQuestIds": [],
"steps": [
{
"id": "gather_intro_step_salvage",
"displayName": "Gather scrap metal",
"objectives": [
{
"id": "gather_intro_obj_scrap",
"kind": "gather_item",
"itemId": "scrap_metal_bulk",
"quantity": 3
}
]
}
]
},
{
"id": "prototype_quest_refine_intro",
"displayName": "Intro: Refine Stock",
"prerequisiteQuestIds": ["prototype_quest_gather_intro"],
"steps": [
{
"id": "refine_intro_step_craft",
"displayName": "Refine scrap",
"objectives": [
{
"id": "refine_intro_obj_recipe",
"kind": "craft_recipe",
"recipeId": "refine_scrap_standard",
"quantity": 1
}
]
}
]
},
{
"id": "prototype_quest_combat_intro",
"displayName": "Intro: Clear the Pocket",
"prerequisiteQuestIds": [],
"steps": [
{
"id": "combat_intro_step_encounter",
"displayName": "Clear the combat pocket",
"objectives": [
{
"id": "combat_intro_obj_encounter",
"kind": "encounter_complete",
"encounterId": "prototype_combat_pocket"
}
]
}
]
},
{
"id": "prototype_quest_operator_chain",
"displayName": "Operator Chain",
"prerequisiteQuestIds": [
"prototype_quest_gather_intro",
"prototype_quest_refine_intro",
"prototype_quest_combat_intro"
],
"steps": [
{
"id": "chain_step_gather",
"displayName": "Bulk salvage",
"objectives": [
{
"id": "chain_obj_gather",
"kind": "gather_item",
"itemId": "scrap_metal_bulk",
"quantity": 5
}
]
},
{
"id": "chain_step_refine",
"displayName": "Refine stock",
"objectives": [
{
"id": "chain_obj_refine",
"kind": "craft_recipe",
"recipeId": "refine_scrap_standard",
"quantity": 1
}
]
},
{
"id": "chain_step_stim",
"displayName": "Craft field stim",
"objectives": [
{
"id": "chain_obj_stim",
"kind": "craft_recipe",
"recipeId": "make_field_stim_mk0",
"quantity": 1
}
]
},
{
"id": "chain_step_token",
"displayName": "Hand off contract token",
"objectives": [
{
"id": "chain_obj_token",
"kind": "inventory_has_item",
"itemId": "contract_handoff_token",
"quantity": 1
}
]
}
]
}
]
}
""";
private static (string Root, string QuestsDir, string DefSchemaPath, string StepSchemaPath, string ObjectiveSchemaPath) CreateTempContentLayout()
{
var root = Directory.CreateTempSubdirectory("neon-sprawl-questcat-");
var questsDir = Path.Combine(root.FullName, "content", "quests");
var schemaDir = Path.Combine(root.FullName, "content", "schemas");
Directory.CreateDirectory(questsDir);
Directory.CreateDirectory(schemaDir);
var defSchemaPath = Path.Combine(schemaDir, "quest-def.schema.json");
var stepSchemaPath = Path.Combine(schemaDir, "quest-step-def.schema.json");
var objectiveSchemaPath = Path.Combine(schemaDir, "quest-objective-def.schema.json");
File.Copy(QuestCatalogTestPaths.DiscoverRepoQuestDefSchemaPath(), defSchemaPath, overwrite: true);
File.Copy(QuestCatalogTestPaths.DiscoverRepoQuestStepDefSchemaPath(), stepSchemaPath, overwrite: true);
File.Copy(QuestCatalogTestPaths.DiscoverRepoQuestObjectiveDefSchemaPath(), objectiveSchemaPath, overwrite: true);
return (root.FullName, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath);
}
private static void WriteCatalog(string questsDir, string catalogJson) =>
File.WriteAllText(Path.Combine(questsDir, "prototype_quests.json"), catalogJson, Encoding.UTF8);
private static JsonObject GetQuestRow(JsonObject catalogRoot, string questId)
{
var quests = catalogRoot["quests"] as JsonArray
?? throw new InvalidOperationException("expected quests array");
foreach (var node in quests)
{
if (node is JsonObject row && row["id"]?.GetValue<string>() == questId)
return row;
}
throw new InvalidOperationException($"quest id not found: {questId}");
}
private static QuestDefinitionCatalog LoadCatalog(
string questsDir,
string defSchemaPath,
string stepSchemaPath,
string objectiveSchemaPath) =>
QuestDefinitionCatalogLoader.Load(
questsDir,
defSchemaPath,
stepSchemaPath,
objectiveSchemaPath,
KnownItemIds,
KnownRecipeIds,
KnownEncounterIds,
NullLogger.Instance);
[Fact]
public void Load_ShouldSucceed_WhenCatalogMatchesRepoPrototypeFile()
{
// Arrange
var questsDir = QuestCatalogTestPaths.DiscoverRepoQuestsDirectory();
var defSchemaPath = QuestCatalogTestPaths.DiscoverRepoQuestDefSchemaPath();
var stepSchemaPath = QuestCatalogTestPaths.DiscoverRepoQuestStepDefSchemaPath();
var objectiveSchemaPath = QuestCatalogTestPaths.DiscoverRepoQuestObjectiveDefSchemaPath();
// Act
var catalog = LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath);
// Assert
Assert.Equal(4, catalog.DistinctQuestCount);
}
[Fact]
public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
WriteCatalog(questsDir, ValidPrototypeCatalogJson);
// Act
var catalog = LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath);
// Assert
Assert.Equal(4, catalog.DistinctQuestCount);
Assert.Equal(1, catalog.CatalogJsonFileCount);
Assert.True(catalog.TryGetQuest("prototype_quest_gather_intro", out var gather));
Assert.NotNull(gather);
Assert.Equal("Intro: Salvage Run", gather!.DisplayName);
Assert.Empty(gather.PrerequisiteQuestIds);
Assert.Single(gather.Steps);
Assert.Equal("gather_item", gather.Steps[0].Objectives[0].Kind);
Assert.True(catalog.TryGetQuest("prototype_quest_operator_chain", out var chain));
Assert.NotNull(chain);
Assert.Equal(4, chain!.Steps.Count);
Assert.Equal("inventory_has_item", chain.Steps[^1].Objectives[0].Kind);
Assert.Equal("contract_handoff_token", chain.Steps[^1].Objectives[0].ItemId);
}
[Fact]
public void Load_ShouldThrow_WhenQuestsIsNotArray()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
File.WriteAllText(
Path.Combine(questsDir, "bad_quests.json"),
"""{"schemaVersion": 1, "quests": "nope"}""",
Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("bad_quests.json", ioe.Message, StringComparison.Ordinal);
Assert.Contains("expected top-level 'quests' array", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenSchemaVersionIsNotOne()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
File.WriteAllText(
Path.Combine(questsDir, "bad_quests.json"),
"""{"schemaVersion": 2, "quests": []}""",
Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("expected schemaVersion 1", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenRowViolatesSchema()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
const string bad = """
{
"schemaVersion": 1,
"quests": [
{
"id": "prototype_quest_gather_intro",
"displayName": "",
"prerequisiteQuestIds": [],
"steps": [
{
"id": "gather_intro_step_salvage",
"displayName": "Gather scrap metal",
"objectives": [
{
"id": "gather_intro_obj_scrap",
"kind": "gather_item",
"itemId": "scrap_metal_bulk",
"quantity": 3
}
]
}
]
}
]
}
""";
File.WriteAllText(Path.Combine(questsDir, "bad_quests.json"), bad, Encoding.UTF8);
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("bad_quests.json", ioe.Message, StringComparison.Ordinal);
Assert.Contains("Quest catalog validation failed", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenDuplicateQuestIdAcrossFiles()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
File.WriteAllText(
Path.Combine(questsDir, "extra_quests.json"),
"""
{
"schemaVersion": 1,
"quests": [
{
"id": "prototype_quest_gather_intro",
"displayName": "Duplicate",
"prerequisiteQuestIds": [],
"steps": [
{
"id": "dup_step",
"displayName": "Dup",
"objectives": [
{
"id": "dup_obj",
"kind": "gather_item",
"itemId": "scrap_metal_bulk",
"quantity": 1
}
]
}
]
}
]
}
""",
Encoding.UTF8);
WriteCatalog(questsDir, ValidPrototypeCatalogJson);
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("duplicate quest id 'prototype_quest_gather_intro'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenDuplicateStepIdWithinQuest()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var gather = GetQuestRow(root, "prototype_quest_gather_intro");
var steps = gather["steps"] as JsonArray ?? throw new InvalidOperationException("expected steps");
steps.Add(steps[0]!.DeepClone());
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("duplicate step id 'gather_intro_step_salvage'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenDuplicateObjectiveIdWithinQuest()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var chain = GetQuestRow(root, "prototype_quest_operator_chain");
var steps = chain["steps"] as JsonArray ?? throw new InvalidOperationException("expected steps");
var firstStep = steps[0] as JsonObject ?? throw new InvalidOperationException("expected step");
var objectives = firstStep["objectives"] as JsonArray ?? throw new InvalidOperationException("expected objectives");
objectives[0]!["id"] = "chain_obj_refine";
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("duplicate objective id 'chain_obj_refine'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenMissingFrozenQuestId()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var quests = root["quests"] as JsonArray ?? throw new InvalidOperationException("expected quests");
quests.RemoveAt(3);
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("prototype E7M1 expects exactly quest ids", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenUnknownPrerequisiteQuestId()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var refine = GetQuestRow(root, "prototype_quest_refine_intro");
refine["prerequisiteQuestIds"] = new JsonArray("prototype_quest_missing");
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("prerequisiteQuestIds references unknown quest id", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenCyclicPrerequisiteQuestIds()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var gather = GetQuestRow(root, "prototype_quest_gather_intro");
gather["prerequisiteQuestIds"] = new JsonArray("prototype_quest_refine_intro");
var refine = GetQuestRow(root, "prototype_quest_refine_intro");
refine["prerequisiteQuestIds"] = new JsonArray("prototype_quest_gather_intro");
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("cyclic prerequisiteQuestIds", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenUnknownItemIdInObjective()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var gather = GetQuestRow(root, "prototype_quest_gather_intro");
var steps = gather["steps"] as JsonArray ?? throw new InvalidOperationException("expected steps");
var step = steps[0] as JsonObject ?? throw new InvalidOperationException("expected step");
var objectives = step["objectives"] as JsonArray ?? throw new InvalidOperationException("expected objectives");
var obj = objectives[0] as JsonObject ?? throw new InvalidOperationException("expected objective");
obj["itemId"] = "unknown_item_id";
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("itemId 'unknown_item_id'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenUnknownRecipeIdInObjective()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var refine = GetQuestRow(root, "prototype_quest_refine_intro");
var steps = refine["steps"] as JsonArray ?? throw new InvalidOperationException("expected steps");
var step = steps[0] as JsonObject ?? throw new InvalidOperationException("expected step");
var objectives = step["objectives"] as JsonArray ?? throw new InvalidOperationException("expected objectives");
var obj = objectives[0] as JsonObject ?? throw new InvalidOperationException("expected objective");
obj["recipeId"] = "unknown_recipe_id";
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("recipeId 'unknown_recipe_id'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenUnknownEncounterIdInObjective()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var combat = GetQuestRow(root, "prototype_quest_combat_intro");
var steps = combat["steps"] as JsonArray ?? throw new InvalidOperationException("expected steps");
var step = steps[0] as JsonObject ?? throw new InvalidOperationException("expected step");
var objectives = step["objectives"] as JsonArray ?? throw new InvalidOperationException("expected objectives");
var obj = objectives[0] as JsonObject ?? throw new InvalidOperationException("expected objective");
obj["encounterId"] = "unknown_encounter_id";
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("encounterId 'unknown_encounter_id'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenChainTerminalObjectiveIsWrongKind()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var chain = GetQuestRow(root, "prototype_quest_operator_chain");
var steps = chain["steps"] as JsonArray ?? throw new InvalidOperationException("expected steps");
var lastStep = steps[^1] as JsonObject ?? throw new InvalidOperationException("expected step");
var objectives = lastStep["objectives"] as JsonArray ?? throw new InvalidOperationException("expected objectives");
var obj = objectives[0] as JsonObject ?? throw new InvalidOperationException("expected objective");
obj["kind"] = "craft_recipe";
obj["recipeId"] = "refine_scrap_standard";
obj["quantity"] = 1;
obj.Remove("itemId");
obj.Remove("encounterId");
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("terminal objective kind must be 'inventory_has_item'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenChainTerminalItemIsWrong()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var chain = GetQuestRow(root, "prototype_quest_operator_chain");
var steps = chain["steps"] as JsonArray ?? throw new InvalidOperationException("expected steps");
var lastStep = steps[^1] as JsonObject ?? throw new InvalidOperationException("expected step");
var objectives = lastStep["objectives"] as JsonArray ?? throw new InvalidOperationException("expected objectives");
var obj = objectives[0] as JsonObject ?? throw new InvalidOperationException("expected objective");
obj["itemId"] = "scrap_metal_bulk";
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("terminal itemId must be 'contract_handoff_token'", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenChainTerminalQuantityIsWrong()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
?? throw new InvalidOperationException("expected object");
var chain = GetQuestRow(root, "prototype_quest_operator_chain");
var steps = chain["steps"] as JsonArray ?? throw new InvalidOperationException("expected steps");
var lastStep = steps[^1] as JsonObject ?? throw new InvalidOperationException("expected step");
var objectives = lastStep["objectives"] as JsonArray ?? throw new InvalidOperationException("expected objectives");
var obj = objectives[0] as JsonObject ?? throw new InvalidOperationException("expected objective");
obj["quantity"] = 2;
WriteCatalog(questsDir, root.ToJsonString());
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("terminal quantity must be 1", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public void Load_ShouldThrow_WhenMissingSchemaFile()
{
// Arrange
var (_, questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath) = CreateTempContentLayout();
WriteCatalog(questsDir, ValidPrototypeCatalogJson);
File.Delete(defSchemaPath);
// Act
var ex = Record.Exception(() => LoadCatalog(questsDir, defSchemaPath, stepSchemaPath, objectiveSchemaPath));
// Assert
var ioe = Assert.IsType<InvalidOperationException>(ex);
Assert.Contains("missing schema file", ioe.Message, StringComparison.Ordinal);
}
[Fact]
public async Task Host_ShouldResolveQuestCatalogFromDi()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/health");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var questCatalog = factory.Services.GetRequiredService<QuestDefinitionCatalog>();
Assert.Equal(4, questCatalog.DistinctQuestCount);
Assert.True(questCatalog.TryGetQuest("prototype_quest_gather_intro", out var gather));
Assert.NotNull(gather);
Assert.Equal("Intro: Salvage Run", gather!.DisplayName);
}
[Fact]
public void Host_ShouldFailStartup_WhenQuestsDirectoryInvalid()
{
// Arrange
var badDir = Path.Combine(Path.GetTempPath(), "neon-sprawl-empty-quests-" + Guid.NewGuid().ToString("n"));
Directory.CreateDirectory(badDir);
try
{
// Act
var ex = Record.Exception(() =>
{
using var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
b.UseSetting("Content:QuestsDirectory", badDir));
factory.CreateClient();
});
// Assert
Assert.NotNull(ex);
Assert.Contains("Quest catalog validation failed", ex.ToString(), StringComparison.Ordinal);
}
finally
{
if (Directory.Exists(badDir))
Directory.Delete(badDir);
}
}
}