From 7a90b46c3b8eb43ff17fd699cc6d56f7d7b84fb3 Mon Sep 17 00:00:00 2001 From: paulc1983 Date: Wed, 16 Sep 2026 16:27:18 +0100 Subject: [PATCH] Trim whitespace from string values in TolerantStringJsonConverter and add unit tests for validation --- .../TolerantStringJsonConverter.cs | 2 +- .../TolerantStringJsonConverterTests.cs | 128 ++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 tests/DfE.CheckPerformanceData.UnitTests/ResultsEnquiry/TolerantStringJsonConverterTests.cs diff --git a/src/DfE.CheckPerformanceData.Application/ResultsEnquiry/TolerantStringJsonConverter.cs b/src/DfE.CheckPerformanceData.Application/ResultsEnquiry/TolerantStringJsonConverter.cs index 8cfddb2da..f21a738fa 100644 --- a/src/DfE.CheckPerformanceData.Application/ResultsEnquiry/TolerantStringJsonConverter.cs +++ b/src/DfE.CheckPerformanceData.Application/ResultsEnquiry/TolerantStringJsonConverter.cs @@ -29,7 +29,7 @@ public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS => reader.TokenType switch { JsonTokenType.Null => string.Empty, - JsonTokenType.String => reader.GetString() ?? string.Empty, + JsonTokenType.String => (reader.GetString() ?? string.Empty).Trim(), // The raw token text, so 5 reads as "5" and not "5.0" via a round-trip through double. JsonTokenType.Number => Encoding.UTF8.GetString( reader.HasValueSequence ? BuffersExtensions.ToArray(reader.ValueSequence) : reader.ValueSpan), diff --git a/tests/DfE.CheckPerformanceData.UnitTests/ResultsEnquiry/TolerantStringJsonConverterTests.cs b/tests/DfE.CheckPerformanceData.UnitTests/ResultsEnquiry/TolerantStringJsonConverterTests.cs new file mode 100644 index 000000000..66e17e925 --- /dev/null +++ b/tests/DfE.CheckPerformanceData.UnitTests/ResultsEnquiry/TolerantStringJsonConverterTests.cs @@ -0,0 +1,128 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using DfE.CheckPerformanceData.Application.ResultsEnquiry; +using DfE.CheckPerformanceData.Infrastructure.BlobStorage; + +namespace DfE.CheckPerformanceData.Application.UnitTests.ResultsEnquiry; + +public sealed class TolerantStringJsonConverterTests +{ + private static readonly JsonSerializerOptions Opts = ResultsEnquiryJson.Options; + + // Helper: deserializes a JSON object with a single "v" property through the production converter. + private static string ReadValue(string json) + => JsonSerializer.Deserialize(json, Opts)!.V; + + [Fact] + public void Reads_a_string_value_as_is() + { + var result = ReadValue("""{ "v": "hello" }"""); + Assert.Equal("hello", result); + } + + [Fact] + public void Reads_null_as_empty_string() + { + var result = ReadValue("""{ "v": null }"""); + Assert.Equal(string.Empty, result); + } + + [Fact] + public void Reads_a_number_as_raw_utf8_bytes() + { + var result = ReadValue("""{ "v": 123 }"""); + Assert.Equal("123", result); + } + + [Fact] + public void Reads_true_as_bool_true_string() + { + var result = ReadValue("""{ "v": true }"""); + Assert.Equal(bool.TrueString, result); + } + + [Fact] + public void Reads_false_as_bool_false_string() + { + var result = ReadValue("""{ "v": false }"""); + Assert.Equal(bool.FalseString, result); + } + + [Fact] + public void Throws_on_unexpected_token_type() + { + // "v" points at a JSON array — TolerantStringJsonConverter does not handle arrays. + Assert.Throws(() => ReadValue("""{ "v": [1,2] }""")); + } + + [Fact] + public void Write_produces_valid_json_string() + { + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream)) + { + JsonSerializer.Serialize(writer, "hello world", Opts); + } + + var json = System.Text.Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("\"hello world\"", json); + } + + // --- FR-005: composite key identity --- + + [Fact] + public void Trims_leading_and_trailing_whitespace_from_string_fields() + { + const string json = """ + [ + { + "CYPMD_ID": "1", + "QAN": " 6037116X ", + "QUAL_NAME": " GCSE (9-1) Bus. Studs ", + "SYLLABUS": " 1BS0 ", + "SESSION": " S2024 ", + "GRADE": " 5 ", + "SOURCE": " 16to19_MAIN " + } + ] + """; + + var records = JsonSerializer.Deserialize>(json, StudentResultsBlobClient.JsonOptions)!; + var record = records[0]; + + Assert.Equal("6037116X", record.Qan); + Assert.Equal("GCSE (9-1) Bus. Studs", record.QualificationName); + Assert.Equal("1BS0", record.SyllabusCode); + Assert.Equal("S2024", record.Session); + Assert.Equal("5", record.Grade); + Assert.Equal("16to19_MAIN", record.SourceFile); + } + + [Fact] + public void CompositeKey_uses_trimmed_values() + { + const string json = """ + [ + { + "CYPMD_ID": "1", + "QAN": " 6037116X ", + "QUAL_NAME": " GCSE ", + "SYLLABUS": " 1BS0 ", + "SESSION": " S2024 ", + "GRADE": " 5 ", + "SOURCE": " 16to19_MAIN " + } + ] + """; + + var record = JsonSerializer.Deserialize>(json, StudentResultsBlobClient.JsonOptions)![0]; + + Assert.Equal("6037116X|S2024|16to19_MAIN", record.CompositeKey); + } + + private sealed class Wrapper + { + [JsonPropertyName("v")] + public string V { get; init; } = string.Empty; + } +}