Desk door tests: a binary or invalid UTF-8 anywhere in a reply is a door failure

Pins bin in note, answerer, next_cursor, a card position (value, nil position, key name) and a record id, and 0xff 0xfe
as text in the same places, through list, read and answer. The door already refuses both (Walk's default branch throws
on Binary; the strict UTF-8 decoder throws on bad bytes), so no source changes: the mutants (Binary written as null;
StrictUtf8 = new(false,false)) go red.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Heimdall 2026-11-11 19:19:04 +00:00
commit 1fb9f62760
2 changed files with 135 additions and 0 deletions

View file

@ -109,4 +109,44 @@ public static class DeskServe
}
public static byte[] FromJson(JsonNode node) => MessagePackSerializer.ConvertFromJson(node.ToJsonString(DeskJson.Options));
// A list page of one entry, as JSON (the shape the door takes), from a vendored CultLib entry file.
public static JsonObject PageOf(JsonNode entry, string mind = "gamecult-chat") =>
JsonNode.Parse($$"""{"schemaVersion":"gamecult.fork.list.v0","mind":"{{mind}}","stamp":"s7","open_count":1,"recorded_count":0,"forks":[{{entry.ToJsonString(DeskJson.Options)}}],"next_cursor":null}""")!.AsObject();
public static JsonObject Entry(string file) =>
JsonNode.Parse(MessagePackSerializer.ConvertToJson(File.ReadAllBytes(Path.Combine(DeskFeedFixturePinTests.Directory, "cultlib-3a51bbab", file + ".msgpack"))))!.AsObject();
// The reply payload exactly as given, asked as a list for "gamecult-chat"; throws what the door throws.
public static async Task<DeskListOutcome> ListPayloadAsync(byte[] payload)
{
using var huginn = Socket(payload);
return await new UnixSocketDeskDoor().ListAsync(huginn.Path, Context, new DeskListRequest("gamecult-chat", "open", null, 50), CancellationToken.None);
}
// The reply payload exactly as given, asked as a read.
public static async Task<DeskReadOutcome> ReadPayloadAsync(byte[] payload)
{
using var huginn = Socket(payload);
return await new UnixSocketDeskDoor().ReadAsync(huginn.Path, Context, "gamecult-chat:question:slot-mark-consent", CancellationToken.None);
}
public static async Task<DeskAnswerOutcome> AnswerPayloadAsync(byte[] payload)
{
using var huginn = Socket(payload);
return await new UnixSocketDeskDoor().AnswerAsync(huginn.Path, Context, DeskReplay.AnswerBody(DeskReplay.ChatFork), CancellationToken.None);
}
// A JSON text becomes MessagePack with the one str `marker` (at most 31 bytes) replaced by `with`: a bin, bytes that
// are not UTF-8. JSON cannot spell either, so the marker stands where the test wants one.
public static byte[] WithMarkerReplaced(JsonNode json, string marker, byte[] with)
{
var bytes = MessagePackSerializer.ConvertFromJson(json.ToJsonString(DeskJson.Options));
var needle = new byte[] { (byte)(0xa0 | marker.Length) }.Concat(System.Text.Encoding.ASCII.GetBytes(marker)).ToArray();
var at = bytes.AsSpan().IndexOf(needle);
Assert.True(at >= 0 && bytes.AsSpan(at + 1).IndexOf(needle) < 0, "the marker is in the bytes exactly once");
return [.. bytes[..at], .. with, .. bytes[(at + needle.Length)..]];
}
public static byte[] Bin(int length) => [0xc4, (byte)length, .. Enumerable.Repeat((byte)'x', length)];
}

View file

@ -0,0 +1,95 @@
using System.Text.Json.Nodes;
using Bifrost.Web.Features.Desk;
using Bifrost.Web.Tests.Support;
namespace Bifrost.Web.Tests;
// The reply's bytes that JSON cannot spell: a MessagePack bin where the contract has text or nil, and text that is not
// UTF-8. Either is a door failure wherever it sits, never a nil or a replacement character Bifrost made up.
public sealed class UnixSocketDeskDoorBytesTests
{
private const string Marker = "@@MARK@@";
// Where a value can stand: a nullable key (note, answerer, next_cursor), a text key, and a card position.
public static TheoryData<string> Positions() => new()
{
"note", "answerer", "next_cursor", "card.raised_at", "card.title", "card.options[0].text", "record.counted_answer_id", "mind", "card_key_name",
};
private static JsonObject PageWithMarkerAt(string position)
{
var page = DeskServe.PageOf(DeskServe.Entry("entry-open-counted"));
var entry = page["forks"]![0]!.AsObject();
switch (position)
{
case "note": entry["record"]!["answers"]![0]!["note"] = Marker; break;
case "answerer": entry["record"]!["answers"]![0]!["answerer"] = Marker; break;
case "next_cursor": page["next_cursor"] = Marker; break;
case "card.raised_at": entry["card"]!["raised_at"] = Marker; break;
case "card.title": entry["card"]!["title"] = Marker; break;
case "card.options[0].text": entry["card"]!["options"]![0]!["text"] = Marker; break;
case "record.counted_answer_id": entry["record"]!["counted_answer_id"] = Marker; break;
case "mind": page["mind"] = Marker; break;
case "card_key_name":
var card = entry["card"]!.AsObject();
var rest = card.ToList();
card.Clear();
foreach (var (key, value) in rest)
{
card[key == "title" ? Marker : key] = value;
}
break;
default: throw new ArgumentOutOfRangeException(nameof(position));
}
return page;
}
[Theory]
[MemberData(nameof(Positions))]
public async Task A_binary_in_a_nullable_text_or_card_position_is_a_door_failure(string position)
{
var bytes = DeskServe.WithMarkerReplaced(PageWithMarkerAt(position), Marker, DeskServe.Bin(3));
await Assert.ThrowsAsync<DeskDoorException>(() => DeskServe.ListPayloadAsync(DeskServe.Forks(bytes)));
}
[Theory]
[MemberData(nameof(Positions))]
public async Task Invalid_utf8_in_text_is_a_door_failure(string position)
{
// 0xff 0xfe as a str of 2 bytes; Bifrost writes no replacement character.
var bytes = DeskServe.WithMarkerReplaced(PageWithMarkerAt(position), Marker, [0xa2, 0xff, 0xfe]);
await Assert.ThrowsAsync<DeskDoorException>(() => DeskServe.ListPayloadAsync(DeskServe.Forks(bytes)));
}
[Fact]
public async Task The_same_marker_as_valid_text_decodes_so_the_failures_above_are_the_bytes_not_the_page()
{
foreach (var position in new[] { "note", "answerer", "next_cursor", "card.raised_at", "card.title" })
{
var bytes = DeskServe.WithMarkerReplaced(PageWithMarkerAt(position), Marker, [.. new byte[] { 0xa2 }, .. "ok"u8.ToArray()]);
Assert.IsType<DeskListOutcome.Listed>(await DeskServe.ListPayloadAsync(DeskServe.Forks(bytes)));
}
}
[Fact]
public async Task A_binary_or_invalid_utf8_in_a_read_or_an_answer_is_a_door_failure_too()
{
var entry = DeskServe.Entry("entry-open-counted");
entry["record"]!["answers"]![0]!["note"] = Marker;
foreach (var with in new[] { DeskServe.Bin(2), new byte[] { 0xa2, 0xff, 0xfe } })
{
var read = DeskServe.WithMarkerReplaced(entry, Marker, with);
await Assert.ThrowsAsync<DeskDoorException>(() => DeskServe.ReadPayloadAsync(DeskServe.Fork(read)));
var answer = DeskServe.Entry("entry-open-counted")["record"]!["answers"]![0]!;
answer["note"] = Marker;
var bytes = DeskServe.WithMarkerReplaced(answer, Marker, with);
await Assert.ThrowsAsync<DeskDoorException>(() => DeskServe.AnswerPayloadAsync(DeskServe.Answered(bytes, "n1")));
}
}
}