mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
933c7eb51c
Every HTTP request the 2023-03-21 client can issue now reaches a handler under
the verb it actually uses: 522/522, with an empty KnownGaps allowlist. The
coverage test asserts it, so a regression fails the build rather than becoming
a 404 in the wild.
The six routes left after the previous pass all needed storage that did not
exist, so they are done here as one schema change rather than stubbed:
- ClubRolePermissionEntity (ClubId, MembershipType, six bools) backs
PUT club/{id}/permissions/{role}. A club with no stored row falls back to the
derived default policy, so every club that predates the table behaves exactly
as before. Partial writes keep the values they currently resolve to, so a
client that omits a field cannot silently revoke a permission.
- ClubAdditionalImageEntity (ClubId, Slot, ImageName) backs the gallery. The
details envelope now emits real {ImageName, Slot} rows instead of an empty
array, and DELETE clears a slot.
- ClubEntity.MinLevel and ClubEntity.ClubChatEnabled back their setters and are
read back by hasDisabledClubChat and the Club wire shape, which previously
reported hardcoded defaults.
- ChatMessageEntity.ModerationState backs PUT thread/message/{id}/moderate,
gated so an author can retract their own message but only club moderators and
admins can action anyone else's.
- TestCaseEntity.CommentsJson backs the QA comment endpoint, appending rather
than overwriting so a second tester's note cannot erase the first.
Also corrects the Joinability comment on ClubEntity, which documented
1=RequestToJoin / 2=InviteOnly. The real enum is Open=0, InviteOnly=1,
AskToJoin=2 — the inverted reading the requesttojoin handler was already fixed
away from, left behind where it could mislead the next change.
116 lines
4.8 KiB
C#
116 lines
4.8 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace DorkNet.Server.Tests;
|
|
|
|
/// <summary>
|
|
/// Asserts that every HTTP request the Rec Room 2023-03-21 client can issue has
|
|
/// a handler on this server, under the verb the client actually uses.
|
|
///
|
|
/// Rec Room is shut down, so the client can't be observed against a live server.
|
|
/// <c>data/client-routes-2023.tsv</c> is the recovered inventory of what it
|
|
/// asks for — see <c>docs/recroom-2023-client-api-complete.md</c> for how it was
|
|
/// extracted. This test is the regression guard on that inventory: adding a
|
|
/// client route without a handler, or moving a handler to a different verb,
|
|
/// fails here rather than silently 404/405-ing a real player.
|
|
///
|
|
/// Routes not yet implemented live in <see cref="KnownGaps"/>. That list is
|
|
/// expected to shrink and never grow. Deleting an entry as you implement it is
|
|
/// the point — it is the project's running score against the client.
|
|
/// </summary>
|
|
public sealed class ClientRouteCoverageTests
|
|
{
|
|
/// <summary>Client routes with no handler yet. Each entry is
|
|
/// <c>VERB route</c> exactly as it appears in the inventory file.
|
|
///
|
|
/// This is EMPTY, and that is the point: every request the 2023-03-21
|
|
/// client can issue reaches a handler under the verb it actually uses. Do
|
|
/// not add an entry to make a red build green — an entry here is a promise
|
|
/// that the client gets a 404 or 405 in the wild. Implement the route, or
|
|
/// if the inventory itself is wrong (a literal that is really a cache key,
|
|
/// a deeplink, or a base the client concatenates onto), fix the inventory
|
|
/// so the claim stays honest.</summary>
|
|
private static readonly HashSet<string> KnownGaps = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
[Fact]
|
|
public void Every_client_route_has_a_handler()
|
|
{
|
|
var client = LoadClientRoutes();
|
|
Assert.NotEmpty(client);
|
|
|
|
var discovered = EndpointContractDiscovery.Discover()
|
|
.Select(r => (Verb: r.Method.ToUpperInvariant(), Pattern: Normalise(r.Template)))
|
|
.ToList();
|
|
|
|
var server = discovered.ToHashSet();
|
|
|
|
// A bare [Route] with no verb attribute serves every verb.
|
|
var verbless = discovered
|
|
.Where(d => d.Verb == "ANY")
|
|
.Select(d => d.Pattern)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
var serverAnyVerb = discovered.Select(s => s.Pattern).ToHashSet(StringComparer.Ordinal);
|
|
|
|
var missing = new List<string>();
|
|
var wrongVerb = new List<string>();
|
|
|
|
foreach (var (verb, route, _) in client)
|
|
{
|
|
var pattern = Normalise(route);
|
|
if (server.Contains((verb, pattern)) || verbless.Contains(pattern)) continue;
|
|
|
|
// Distinguish "no handler at all" from "handler exists, wrong verb" —
|
|
// they have different causes and different fixes.
|
|
if (serverAnyVerb.Contains(pattern)) wrongVerb.Add($"{verb} {route}");
|
|
else missing.Add($"{verb} {route}");
|
|
}
|
|
|
|
var gaps = missing.Concat(wrongVerb)
|
|
.Where(g => !KnownGaps.Contains(g))
|
|
.OrderBy(g => g, StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
var covered = client.Count - missing.Count - wrongVerb.Count;
|
|
var pct = 100.0 * covered / client.Count;
|
|
|
|
Assert.True(gaps.Count == 0,
|
|
$"""
|
|
Client route coverage: {covered}/{client.Count} ({pct:F1}%).
|
|
{missing.Count} missing, {wrongVerb.Count} wrong-verb, {KnownGaps.Count} known gaps.
|
|
|
|
Unlisted gaps ({gaps.Count}) — implement them, or add to KnownGaps with a reason:
|
|
{string.Join(Environment.NewLine, gaps)}
|
|
""");
|
|
}
|
|
|
|
/// <summary>Collapse a route to a comparable shape: no host, no leading or
|
|
/// trailing slash, no query, and every parameter — the client's <c>{0}</c>
|
|
/// and the server's <c>{id:long}</c> alike — reduced to <c>*</c>.</summary>
|
|
private static string Normalise(string route)
|
|
{
|
|
var r = route.Trim();
|
|
var q = r.IndexOf('?');
|
|
if (q >= 0) r = r[..q];
|
|
r = r.Trim('/', '~');
|
|
r = Regex.Replace(r, @"\{[^}]*\}", "*");
|
|
return r.ToLowerInvariant();
|
|
}
|
|
|
|
private static List<(string Verb, string Route, string Subsystem)> LoadClientRoutes()
|
|
{
|
|
var path = Path.Combine(AppContext.BaseDirectory, "data", "client-routes-2023.tsv");
|
|
Assert.True(File.Exists(path), $"missing route inventory at {path}");
|
|
|
|
var rows = new List<(string, string, string)>();
|
|
foreach (var line in File.ReadLines(path))
|
|
{
|
|
if (line.Length == 0 || line[0] == '#') continue;
|
|
var parts = line.Split('\t');
|
|
if (parts.Length < 2) continue;
|
|
rows.Add((parts[0].Trim().ToUpperInvariant(), parts[1].Trim(),
|
|
parts.Length > 2 ? parts[2].Trim() : string.Empty));
|
|
}
|
|
return rows;
|
|
}
|
|
}
|