566 lines
25 KiB
C#
566 lines
25 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_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_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);
|
|
}
|
|
}
|
|
}
|