Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions.Tests/EventDataNormalizationTests.cs
T
Oliver Kieselbach 98d87a8579 Build: unused private members, unread fields and unused parameters fail the .NET build
The root .editorconfig raises IDE0051, IDE0052 and IDE0060 (non-public
methods) to warning, and Directory.Build.props sets EnforceCodeStyleInBuild,
so the warning freeze turns them into build errors in every project, the net48
agent included.

Cleanup to start at zero, each finding checked for leftover vs. missing
behaviour (none was a feature that silently never ran):
- 38 injected dependencies nobody read: loggers, configuration, and services
  whose last read had moved to a live replacement (the IME reseed writes
  through the rule repository, the offboarding poison OpsEvent comes from the
  handler, the vulnerability gate sits in the correlate handler). Removed with
  their constructor parameters and the argument at every construction site.
- unused private members: SelfUpdater.IsNewerVersion, a GatherRuleExecutor
  wrapper, test constants and an unused test overload.
  RjPhaseCompletedFirstDeployment now replaces the two literal 110s it names.
- unused parameters removed together with their arguments; test fakes bound as
  method groups use discards.
- the commented-out backend-URL connectivity check and its plumbing are gone;
  the reason the backend stays out remains as a comment.
- DiagnosticsPackageService names the blob in its upload-failure warnings, as
  its doc already said.
- stale docs fixed on the way: OnboardedAt no longer claims a maintenance
  backfill (shared manifest and wire types regenerated), the vulnerability
  functions' policy comments match the catalog, a misplaced summary moved to
  its method.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:03:00 +02:00

119 lines
5.1 KiB
C#

using System.Collections.Generic;
using AutopilotMonitor.Functions.Functions.Ingest;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared.Models;
using Newtonsoft.Json;
using Stj = System.Text.Json.JsonSerializer;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Regression tests for the V2 ingest data-corruption bug: the telemetry payload parser used to
/// skip the Newtonsoft-JToken → native normalization the V1 NDJSON path applied, so nested
/// arrays/objects (e.g. hardware_spec.disks) reached System.Text.Json downstream as JObject/JArray
/// and serialized to corrupt nested-empty-array output. Also covers DetectHasSSD, which used an
/// exact mediaType compare that never matched the agent's composite values ("NVMe SSD").
/// </summary>
public class EventDataNormalizationTests
{
private const string TenantId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
private const string SessionId = "b2c3d4e5-f6a7-8901-bcde-f12345678901";
private static TelemetryItemDto EventDto(string payloadJson)
=> new TelemetryItemDto
{
Kind = "Event",
PartitionKey = $"{TenantId}_{SessionId}",
RowKey = "0000000001",
TelemetryItemId = 1,
PayloadJson = payloadJson,
EnqueuedAtUtc = new System.DateTime(2026, 4, 21, 10, 0, 0, System.DateTimeKind.Utc),
};
// hardware_spec shape the agent actually emits — a nested object array (disks) and a nested
// object array (gpus), plus flat scalars.
private const string HardwareSpecPayload =
"{\"EventType\":\"hardware_spec\",\"Data\":{" +
"\"systemModel\":\"HP EliteBook 840 G8\"," +
"\"ramTotalGB\":7.7," +
"\"disks\":[{\"model\":\"KIOXIA\",\"sizeGB\":238,\"mediaType\":\"NVMe SSD\",\"busType\":\"NVMe\"}]," +
"\"gpus\":[{\"name\":\"Intel Iris Xe\",\"adapterRAMGB\":2}]" +
"}}";
[Fact]
public void ParseEvent_normalizes_nested_objects_to_native_types()
{
var evt = TelemetryPayloadParser.ParseEvent(EventDto(HardwareSpecPayload));
Assert.NotNull(evt);
// Nested array must be a native List<object> of Dictionary<string,object>, NOT a JArray/JObject.
var disks = Assert.IsType<List<object>>(evt!.Data["disks"]);
var disk = Assert.IsType<Dictionary<string, object>>(disks[0]);
Assert.Equal("NVMe SSD", disk["mediaType"]?.ToString());
}
[Fact]
public void ParseEvent_output_is_SystemTextJson_safe()
{
// The actual corruption manifests when the downstream DeviceSnapshot writer re-serializes
// Data with System.Text.Json. Before the fix this produced "disks":[[[[]],...]]; now the
// real fields must survive a round-trip.
var evt = TelemetryPayloadParser.ParseEvent(EventDto(HardwareSpecPayload));
Assert.NotNull(evt);
var json = Stj.Serialize(evt!.Data);
Assert.Contains("\"mediaType\":\"NVMe SSD\"", json);
Assert.Contains("\"model\":\"KIOXIA\"", json);
Assert.Contains("\"name\":\"Intel Iris Xe\"", json);
// The corruption signature: nested empty arrays where objects should be.
Assert.DoesNotContain("[[[", json);
}
[Fact]
public void NormalizeMap_converts_newtonsoft_tokens_from_stored_json()
{
// Mirrors the read path: storage JSON deserialized by Newtonsoft into object values.
var deserialized = JsonConvert.DeserializeObject<Dictionary<string, object>>(
"{\"disks\":[{\"mediaType\":\"NVMe SSD\"}],\"flat\":\"x\"}");
var normalized = EventDataNormalizer.NormalizeMap(deserialized);
Assert.IsType<List<object>>(normalized["disks"]);
Assert.Equal("x", normalized["flat"]?.ToString());
Assert.DoesNotContain("[[[", Stj.Serialize(normalized));
}
[Fact]
public void NormalizeMap_returns_empty_for_null()
=> Assert.Empty(EventDataNormalizer.NormalizeMap(null));
// ============================================================ DetectHasSSD
private static Dictionary<string, object> DisksWith(params string[] mediaTypes)
{
var disks = new List<object>();
foreach (var mt in mediaTypes)
disks.Add(new Dictionary<string, object> { ["mediaType"] = mt });
return new Dictionary<string, object> { ["disks"] = disks };
}
[Theory]
[InlineData("NVMe SSD", true)] // composite value the exact-equals bug missed
[InlineData("NVMe", true)]
[InlineData("SSD", true)]
[InlineData("nvme ssd", true)] // case-insensitive
[InlineData("HDD", false)]
[InlineData("Unknown", false)]
public void DetectHasSSD_matches_composite_and_case_insensitive(string mediaType, bool expected)
=> Assert.Equal(expected, TableStorageService.DetectHasSSD(DisksWith(mediaType)));
[Fact]
public void DetectHasSSD_true_when_any_disk_is_solid_state()
=> Assert.Equal(true, TableStorageService.DetectHasSSD(DisksWith("Unknown", "NVMe SSD")));
[Fact]
public void DetectHasSSD_null_when_no_disks_key()
=> Assert.Null(TableStorageService.DetectHasSSD(new Dictionary<string, object>()));
}