Files
ersatztv/ErsatzTV/Serialization/ApiJsonSettings.cs
T
timothyandClaude Fable 5.1 3df98d247f docs(563): state each binder test's mechanism instead of its mutant colour, per the amended CLAIMS rule
`testing.mutation-claims-are-executed` was amended on main while this branch was in review (#881,
merged as #914): a sentence asserting that a specific mutation reddens — or does not redden — a named
test is now either a `CLAIMS` entry in `scripts/tests/mutation_manifest.py` that executes every run,
or it is not written. This branch carried six such sentences and none of them can be declared:
`Claim.node_id` resolves a proof to `scripts/tests/<node id>` and `run_pytest` invokes pytest, so an
NUnit proof has no representation in that harness at all.

Durable prose now states the mechanism each test is built on — which serializer difference, which
engine branch — which a reader re-checks by reading the code rather than by trusting a remembered
outcome. The record says that in one paragraph, so the limit is stated rather than papered over.

The outcomes themselves are here. Re-measured 2026-09-05 on this branch's tree (the commit before
this one), each mutant applied to the working tree and restored from the index between runs, tree
verified clean afterwards:

  positive control          ScriptedScheduleControllerTests   Passed: 9, Failed: 0
                            OpenApiSerializerContractTests    Passed: 4, Failed: 0
  Bind<T> -> System.Text.Json with JsonSerializerDefaults.Web
                            Failed: 2, Passed: 7 — Production_Body_Binder_Ignores_Required_Members,
                            Production_Body_Binder_Keeps_Declared_Defaults_Over_An_Explicit_Null
  BodyBinderSettings = ApiJsonSettings.Create() -> new JsonSerializerSettings()
                            Failed: 1, Passed: 8 —
                            Production_Body_Binder_Keeps_Declared_Defaults_Over_An_Explicit_Null
  OpenApiSerializerContractTests RuntimeSettings -> new JsonSerializerSettings()
                            Failed: 4, Passed: 0 — all four cases, on PascalCase keys
  ScriptedScheduleController AddDuration(..., request.Trim, ...) -> false
                            Failed: 1, Passed: 8 — Committed_Script_Fixture_Produces_The_Pinned_Snapshot
  ScriptedScheduleController PadUntilExact(..., request.Trim, ...) -> false
                            Failed: 1, Passed: 8 — Committed_Script_Fixture_Produces_The_Pinned_Snapshot

The last one is the round-two finding closed and re-witnessed: before the fixture's pad target moved
off the content boundary, that mutant left all nine green.

A squash merge writes its own message, so these figures also belong in the PR description.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 09:31:42 +02:00

44 lines
2.7 KiB
C#

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace ErsatzTV.Serialization;
/// <summary>
/// The Newtonsoft settings the MVC JSON formatters read and write `/api/*` bodies with. Defined once,
/// here, so a test can deserialize a request body exactly the way the production body binder does
/// instead of mirroring <see cref="Startup" />'s registration. This REMOVES the duplicate rather than
/// detecting drift in one: no test observes <c>Startup.ConfigureServices</c>, and a byte-equal hand-copy
/// of <see cref="Apply" /> is behaviourally indistinguishable from calling it. What the tests do
/// separate is a caller that binds with something else — a body omitting a C#
/// <c>required</c> member deserializes to a default here where System.Text.Json throws, and
/// <see cref="NullValueHandling" />.<c>Ignore</c> keeps a DTO's declared default over an explicit
/// null where plain Newtonsoft writes the null through. Both are pinned by
/// <c>ScriptedScheduleControllerTests</c>; the naming strategy is pinned on the write side by
/// <c>OpenApiSerializerContractTests</c>. See docs/testing.md → "Scripted playout coverage".
/// </summary>
public static class ApiJsonSettings
{
/// <summary>Applies the production configuration to an existing settings object (the MVC path).</summary>
public static JsonSerializerSettings Apply(JsonSerializerSettings settings)
{
settings.NullValueHandling = NullValueHandling.Ignore;
settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
settings.ContractResolver = new CustomContractResolver();
settings.Converters.Add(new StringEnumConverter());
return settings;
}
/// <summary>
/// <see cref="Apply" /> onto a bare settings object, for a caller outside the MVC pipeline. It carries
/// everything <see cref="Apply" /> sets and nothing MVC supplies around it, so it is NOT the object the
/// body binder runs. Measured 2026-09-05 against <c>new MvcNewtonsoftJsonOptions().SerializerSettings</c>:
/// <c>MaxDepth</c> is Newtonsoft's 64 rather than MVC's stricter 32, and MVC's two error-shaping
/// converters — <c>ProblemDetailsConverter</c> and <c>ValidationProblemDetailsConverter</c> — are absent.
/// <c>MissingMemberHandling</c>, <c>TypeNameHandling</c> and <c>DateParseHandling</c> match. Both gaps are
/// inert for the request DTOs the tests bind (two levels deep, never a <c>ProblemDetails</c>), which is why
/// this is usable there at all; the delta is pinned by <c>ApiJsonSettingsTests</c> so it cannot decay into
/// a parity claim.
/// </summary>
public static JsonSerializerSettings Create() => Apply(new JsonSerializerSettings());
}