NEO-36: expose GET /game/world/skill-definitions with tests, Bruno, QA doc
parent
044b4f823c
commit
ab597ff350
|
|
@ -0,0 +1,46 @@
|
||||||
|
meta {
|
||||||
|
name: GET skill definitions
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/game/world/skill-definitions
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("returns 200 JSON with schema v1 and skills array", function () {
|
||||||
|
expect(res.getStatus()).to.equal(200);
|
||||||
|
expect(res.getHeader("content-type")).to.contain("application/json");
|
||||||
|
const body = res.getBody();
|
||||||
|
expect(body.schemaVersion).to.equal(1);
|
||||||
|
expect(body.skills).to.be.an("array");
|
||||||
|
expect(body.skills.length).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skills are ascending by id (ordinal)", function () {
|
||||||
|
const body = res.getBody();
|
||||||
|
const ids = body.skills.map((x) => x.id);
|
||||||
|
const sorted = [...ids].sort((a, b) => a.localeCompare(b));
|
||||||
|
expect(ids).to.eql(sorted);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("frozen prototype trio is present", function () {
|
||||||
|
const body = res.getBody();
|
||||||
|
const ids = new Set(body.skills.map((x) => x.id));
|
||||||
|
expect(ids.has("salvage")).to.equal(true);
|
||||||
|
expect(ids.has("refine")).to.equal(true);
|
||||||
|
expect(ids.has("intrusion")).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("salvage row matches catalog", function () {
|
||||||
|
const body = res.getBody();
|
||||||
|
const row = body.skills.find((x) => x.id === "salvage");
|
||||||
|
expect(row).to.be.an("object");
|
||||||
|
expect(row.displayName).to.equal("Salvage");
|
||||||
|
expect(row.category).to.equal("gather");
|
||||||
|
expect(row.allowedXpSourceKinds).to.include.members(["activity", "mission_reward"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
meta {
|
||||||
|
name: skill-definitions
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Manual QA — NEO-36 (read-only skill catalog HTTP)
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- Server built and configured with default `Content:SkillsDirectory` pointing at repo `content/skills` (local dev / `InMemoryWebApplicationFactory` tests use the same layout).
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
1. Start **`NeonSprawl.Server`** (e.g. `dotnet run` from `server/NeonSprawl.Server`).
|
||||||
|
2. **`GET /game/world/skill-definitions`** — expect **200** and **`Content-Type`** containing **`application/json`**.
|
||||||
|
3. Parse JSON — expect **`schemaVersion`** === **1**, **`skills`** array length **3**.
|
||||||
|
4. Confirm **`id`** values include **`intrusion`**, **`refine`**, **`salvage`** (frozen prototype trio).
|
||||||
|
5. Spot-check **`salvage`**: **`displayName`** “Salvage”, **`category`** **`gather`**, **`allowedXpSourceKinds`** includes **`activity`** and **`mission_reward`**.
|
||||||
|
6. Optional: run **`bruno/neon-sprawl-server/skill-definitions/Get skill definitions.bru`** against the same **`baseUrl`** (see `environments/Local.bru`).
|
||||||
|
|
@ -35,8 +35,8 @@ No questions were put to the user during kickoff.
|
||||||
|
|
||||||
## Acceptance criteria checklist
|
## Acceptance criteria checklist
|
||||||
|
|
||||||
- [ ] Bruno (and/or automated test) can call the endpoint against a running dev server and observe the **three frozen ids** (`salvage`, `refine`, `intrusion`).
|
- [x] Bruno (and/or automated test) can call the endpoint against a running dev server and observe the **three frozen ids** (`salvage`, `refine`, `intrusion`).
|
||||||
- [ ] `docs/manual-qa/NEO-36.md` exists with a short checklist for hitting the endpoint.
|
- [x] `docs/manual-qa/NEO-36.md` exists with a short checklist for hitting the endpoint.
|
||||||
|
|
||||||
## Technical approach
|
## Technical approach
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using NeonSprawl.Server.Game.Skills;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace NeonSprawl.Server.Tests.Game.Skills;
|
||||||
|
|
||||||
|
public class SkillDefinitionsWorldApiTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSkillDefinitions_ShouldReturnSchemaV1_WithFrozenTrioInIdOrder()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("/game/world/skill-definitions");
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var body = await response.Content.ReadFromJsonAsync<SkillDefinitionsListResponse>();
|
||||||
|
Assert.NotNull(body);
|
||||||
|
Assert.Equal(SkillDefinitionsListResponse.CurrentSchemaVersion, body!.SchemaVersion);
|
||||||
|
Assert.NotNull(body.Skills);
|
||||||
|
var ids = body.Skills.Select(static s => s.Id).ToList();
|
||||||
|
Assert.Equal(new[] { "intrusion", "refine", "salvage" }, ids);
|
||||||
|
|
||||||
|
var salvage = body.Skills.Single(s => s.Id == "salvage");
|
||||||
|
Assert.Equal("Salvage", salvage.DisplayName);
|
||||||
|
Assert.Equal("gather", salvage.Category);
|
||||||
|
Assert.Equal(new[] { "activity", "mission_reward" }, salvage.AllowedXpSourceKinds);
|
||||||
|
|
||||||
|
var refine = body.Skills.Single(s => s.Id == "refine");
|
||||||
|
Assert.Contains("trainer", refine.AllowedXpSourceKinds);
|
||||||
|
|
||||||
|
var intrusion = body.Skills.Single(s => s.Id == "intrusion");
|
||||||
|
Assert.Contains("book_or_item", intrusion.AllowedXpSourceKinds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NeonSprawl.Server.Game.Skills;
|
||||||
|
|
||||||
|
/// <summary>JSON body for <c>GET /game/world/skill-definitions</c> (NEO-36).</summary>
|
||||||
|
public sealed class SkillDefinitionsListResponse
|
||||||
|
{
|
||||||
|
public const int CurrentSchemaVersion = 1;
|
||||||
|
|
||||||
|
[JsonPropertyName("schemaVersion")]
|
||||||
|
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
|
||||||
|
|
||||||
|
/// <summary>Loaded skills ordered by stable <c>id</c> (ordinal), matching <see cref="ISkillDefinitionRegistry.GetDefinitionsInIdOrder"/>.</summary>
|
||||||
|
[JsonPropertyName("skills")]
|
||||||
|
public required IReadOnlyList<SkillDefinitionJson> Skills { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>One row in the read-only skill definition projection.</summary>
|
||||||
|
public sealed class SkillDefinitionJson
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public required string Id { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("displayName")]
|
||||||
|
public required string DisplayName { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("category")]
|
||||||
|
public required string Category { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("allowedXpSourceKinds")]
|
||||||
|
public required IReadOnlyList<string> AllowedXpSourceKinds { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
namespace NeonSprawl.Server.Game.Skills;
|
||||||
|
|
||||||
|
/// <summary>Maps <c>GET /game/world/skill-definitions</c> (NEO-36).</summary>
|
||||||
|
public static class SkillDefinitionsWorldApi
|
||||||
|
{
|
||||||
|
public static WebApplication MapSkillDefinitionsWorldApi(this WebApplication app)
|
||||||
|
{
|
||||||
|
app.MapGet(
|
||||||
|
"/game/world/skill-definitions",
|
||||||
|
(ISkillDefinitionRegistry registry) =>
|
||||||
|
{
|
||||||
|
var defs = registry.GetDefinitionsInIdOrder();
|
||||||
|
var skills = new List<SkillDefinitionJson>(defs.Count);
|
||||||
|
foreach (var d in defs)
|
||||||
|
{
|
||||||
|
skills.Add(
|
||||||
|
new SkillDefinitionJson
|
||||||
|
{
|
||||||
|
Id = d.Id,
|
||||||
|
DisplayName = d.DisplayName,
|
||||||
|
Category = d.Category,
|
||||||
|
AllowedXpSourceKinds = d.AllowedXpSourceKinds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Json(
|
||||||
|
new SkillDefinitionsListResponse
|
||||||
|
{
|
||||||
|
SchemaVersion = SkillDefinitionsListResponse.CurrentSchemaVersion,
|
||||||
|
Skills = skills,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ app.MapGet("/health", () => Results.Json(new
|
||||||
app.MapPositionStateApi();
|
app.MapPositionStateApi();
|
||||||
app.MapInteractionApi();
|
app.MapInteractionApi();
|
||||||
app.MapInteractablesWorldApi();
|
app.MapInteractablesWorldApi();
|
||||||
|
app.MapSkillDefinitionsWorldApi();
|
||||||
app.MapTargetingApi();
|
app.MapTargetingApi();
|
||||||
app.MapHotbarLoadoutApi();
|
app.MapHotbarLoadoutApi();
|
||||||
app.MapCooldownSnapshotApi();
|
app.MapCooldownSnapshotApi();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue