200 lines
8.0 KiB
C#
200 lines
8.0 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using NeonSprawl.Server.Tests;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NeonSprawl.Server.Game.Skills;
|
|
using Xunit;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.Skills;
|
|
|
|
public class SkillDefinitionCatalogLoaderTests
|
|
{
|
|
private const string ValidPrototypeCatalogJson =
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"skills": [
|
|
{
|
|
"id": "salvage",
|
|
"category": "gather",
|
|
"displayName": "Salvage",
|
|
"allowedXpSourceKinds": ["activity", "mission_reward"]
|
|
},
|
|
{
|
|
"id": "refine",
|
|
"category": "process",
|
|
"displayName": "Refine",
|
|
"allowedXpSourceKinds": ["activity", "mission_reward", "trainer"]
|
|
},
|
|
{
|
|
"id": "intrusion",
|
|
"category": "tech",
|
|
"displayName": "Intrusion",
|
|
"allowedXpSourceKinds": ["activity", "mission_reward", "book_or_item"]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
|
|
private static (string Root, string SkillsDir, string SchemaPath) CreateTempContentLayout()
|
|
{
|
|
var root = Directory.CreateTempSubdirectory("neon-sprawl-skillcat-");
|
|
var skillsDir = Path.Combine(root.FullName, "content", "skills");
|
|
var schemaDir = Path.Combine(root.FullName, "content", "schemas");
|
|
Directory.CreateDirectory(skillsDir);
|
|
Directory.CreateDirectory(schemaDir);
|
|
var schemaPath = Path.Combine(schemaDir, "skill-def.schema.json");
|
|
File.Copy(SkillCatalogTestPaths.DiscoverRepoSkillDefSchemaPath(), schemaPath, overwrite: true);
|
|
return (root.FullName, skillsDir, schemaPath);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
|
|
{
|
|
// Arrange
|
|
var (_, skillsDir, schemaPath) = CreateTempContentLayout();
|
|
File.WriteAllText(Path.Combine(skillsDir, "prototype.json"), ValidPrototypeCatalogJson, Encoding.UTF8);
|
|
// Act
|
|
var catalog = SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance);
|
|
// Assert
|
|
Assert.Equal(3, catalog.DistinctSkillCount);
|
|
Assert.Equal(1, catalog.CatalogJsonFileCount);
|
|
Assert.True(catalog.ById.ContainsKey("salvage"));
|
|
Assert.Equal("gather", catalog.ById["salvage"].Category);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenSkillsIsNotArray()
|
|
{
|
|
// Arrange
|
|
var (_, skillsDir, schemaPath) = CreateTempContentLayout();
|
|
File.WriteAllText(Path.Combine(skillsDir, "bad.json"), """{"skills": "nope"}""", Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("bad.json", ioe.Message, StringComparison.Ordinal);
|
|
Assert.Contains("expected top-level 'skills' array", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenRowViolatesSchema()
|
|
{
|
|
// Arrange
|
|
var (_, skillsDir, schemaPath) = CreateTempContentLayout();
|
|
var bad = """
|
|
{
|
|
"skills": [
|
|
{
|
|
"id": "salvage",
|
|
"category": "gather",
|
|
"displayName": "",
|
|
"allowedXpSourceKinds": ["activity"]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(skillsDir, "bad.json"), bad, Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("bad.json", ioe.Message, StringComparison.Ordinal);
|
|
Assert.Contains("Skill catalog validation failed", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenDuplicateIdAcrossFiles()
|
|
{
|
|
// Arrange
|
|
var (_, skillsDir, schemaPath) = CreateTempContentLayout();
|
|
var singleSalvage = """
|
|
{
|
|
"skills": [
|
|
{
|
|
"id": "salvage",
|
|
"category": "gather",
|
|
"displayName": "Salvage",
|
|
"allowedXpSourceKinds": ["activity"]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(skillsDir, "a.json"), singleSalvage, Encoding.UTF8);
|
|
File.WriteAllText(Path.Combine(skillsDir, "b.json"), singleSalvage, Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("duplicate skill id", ioe.Message, StringComparison.Ordinal);
|
|
Assert.Contains("salvage", ioe.Message, StringComparison.Ordinal);
|
|
Assert.Contains("a.json", ioe.Message, StringComparison.Ordinal);
|
|
Assert.Contains("b.json", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenSlice1IdsIncomplete()
|
|
{
|
|
// Arrange
|
|
var (_, skillsDir, schemaPath) = CreateTempContentLayout();
|
|
var twoOnly = """
|
|
{
|
|
"skills": [
|
|
{
|
|
"id": "salvage",
|
|
"category": "gather",
|
|
"displayName": "Salvage",
|
|
"allowedXpSourceKinds": ["activity"]
|
|
},
|
|
{
|
|
"id": "refine",
|
|
"category": "process",
|
|
"displayName": "Refine",
|
|
"allowedXpSourceKinds": ["activity"]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
File.WriteAllText(Path.Combine(skillsDir, "partial.json"), twoOnly, Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("prototype Slice 1 expects exactly skill ids", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Host_ShouldResolveCatalogFromDi_WhenStartupSucceeds()
|
|
{
|
|
// Arrange — InMemoryWebApplicationFactory strips Postgres and pins Content:SkillsDirectory to the repo catalog.
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
using var client = factory.CreateClient();
|
|
// Act
|
|
var response = await client.GetAsync("/health");
|
|
// Assert
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var catalog = factory.Services.GetRequiredService<SkillDefinitionCatalog>();
|
|
Assert.Equal(3, catalog.DistinctSkillCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Host_ShouldFailStartup_WhenCatalogDirectoryInvalid()
|
|
{
|
|
// Arrange
|
|
var badDir = Path.Combine(Path.GetTempPath(), "neon-sprawl-empty-skills-" + Guid.NewGuid().ToString("n"));
|
|
Directory.CreateDirectory(badDir);
|
|
// Act
|
|
var ex = Record.Exception(() =>
|
|
{
|
|
using var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
|
|
b.UseSetting("Content:SkillsDirectory", badDir));
|
|
factory.CreateClient();
|
|
});
|
|
// Assert
|
|
Assert.NotNull(ex);
|
|
Assert.Contains("Skill catalog validation failed", ex.ToString(), StringComparison.Ordinal);
|
|
}
|
|
}
|