From 757427ffeaff6a24e2d0481e1f9af7f755a0ca94 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 22 Jul 2026 11:09:11 +0200 Subject: [PATCH] chore(build): clear the compiler and xUnit analyzer warnings CI annotates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI test job surfaced 10 annotations, but GitHub caps them at 10 per job — a full rebuild showed 31 nullable warnings plus 26 xUnit analyzer warnings. Production code: - CertificateValidator: X509Certificate2(byte[]) is obsolete (SYSLIB0057) -> X509CertificateLoader.LoadCertificate. - AuthenticationMiddleware: IsValidTenantId now carries [NotNullWhen(true)], so the guard above BuildTenantAuthority is visible to flow analysis. - AgentEndpointMigrationRules: the out params are genuinely nullable on the false path; netstandard2.0 has no usable [NotNullWhen], so the contract is documented instead. - Constants.ClassifyEspAppsFailure: errorCode is optional by design (the "no HRESULT" branch is the timeout fallback) - signature says so now. Tests: nullable annotations on helper signatures, Assert.Single/DoesNotContain overloads instead of pre-filtering with Where, and [InlineData(null)] theories declare nullable parameters. GatherRule helpers now seed Parameters with the model default (empty) rather than null, matching a deserialized rule. No behaviour change. 5616 tests pass; solution builds warning-free apart from the two pre-existing NU1510 pruning notices on the pinned security packages. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Configuration/EndpointMigrationTests.cs | 2 +- .../RemoteConfigServiceCacheHardeningTests.cs | 1 + .../RemoteConfigServiceRetryTests.cs | 6 ++--- .../ShellCoreToEngineEspExitingTests.cs | 2 +- .../Gather/GatherRuleCollectorGuardTests.cs | 8 ++++--- .../Gather/GatherRuleExecutorEmitModeTests.cs | 2 +- .../GatherRuleExecutorPhaseExitTests.cs | 2 +- .../GatherRuleExecutorPhaseScopeTests.cs | 4 ++-- .../Monitoring/Ime/CmTraceLogParserTests.cs | 2 +- .../Monitoring/Ime/ImeLogPatternReDoSTests.cs | 5 ++-- .../SystemSignals/AppxFailureAnalyzerTests.cs | 2 +- .../ProvisioningFailureSettleWindowTests.cs | 18 +++++++------- ...ecisionStepProcessorParkedTripwireTests.cs | 21 +++++++++++----- .../LifecycleEmittersRemoteConfigTests.cs | 4 ++-- .../EspFailureDetectedHResultTests.cs | 2 +- .../ImeLogTrackerAdapterTimingTests.cs | 2 +- .../Telemetry/BandwidthEstimatorTests.cs | 24 +++++++++---------- .../Middleware/AuthenticationMiddleware.cs | 3 ++- .../Security/CertificateValidator.cs | 2 +- .../AutopilotMonitor.Shared/Constants.cs | 6 +++-- .../Services/AgentEndpointMigrationRules.cs | 9 ++++--- 21 files changed, 73 insertions(+), 54 deletions(-) diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/EndpointMigrationTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/EndpointMigrationTests.cs index e7d34c4b..b59aa084 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/EndpointMigrationTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/EndpointMigrationTests.cs @@ -15,7 +15,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Configuration private const string CurrentUrl = "https://autopilotmonitor-api-eu.azurewebsites.net"; private const string TargetUrl = "https://autopilotmonitor-api-us.azurewebsites.net"; - private static AgentConfigResponse Config(string migrateUrl) => + private static AgentConfigResponse Config(string? migrateUrl) => new AgentConfigResponse { MigrateToApiBaseUrl = migrateUrl }; [Fact] diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceCacheHardeningTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceCacheHardeningTests.cs index 115d80dc..2cf470af 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceCacheHardeningTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceCacheHardeningTests.cs @@ -70,6 +70,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Configuration // Read the file the production code actually wrote and deserialize with the same lib. var raw = File.ReadAllText(_cachePath); var persisted = JsonConvert.DeserializeObject(raw); + Assert.NotNull(persisted); Assert.False(persisted.UnrestrictedMode); Assert.False(persisted.AllowAgentDowngrade); diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceRetryTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceRetryTests.cs index 363770da..bb7a260e 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceRetryTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceRetryTests.cs @@ -193,7 +193,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Configuration case BackendBehaviour.AlwaysAuthFailure401: throw new BackendAuthException("Unauthorized", 401); case BackendBehaviour.AlwaysReturnNull: - return Task.FromResult(null); + return Task.FromResult(null!); default: throw new InvalidOperationException("unknown behaviour"); } @@ -204,9 +204,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Configuration // In-memory + per-instance cache so a prior test's successful fetch can't // bleed into a later test that expects UsedDefaults. - private AgentConfigResponse _cached; + private AgentConfigResponse? _cached; protected override void CacheConfig(AgentConfigResponse config) { _cached = config; } - protected override AgentConfigResponse LoadCachedConfig() => _cached; + protected override AgentConfigResponse LoadCachedConfig() => _cached!; } } } diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Integration/ShellCoreToEngineEspExitingTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Integration/ShellCoreToEngineEspExitingTests.cs index b1243b35..cd8eead4 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Integration/ShellCoreToEngineEspExitingTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Integration/ShellCoreToEngineEspExitingTests.cs @@ -51,7 +51,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Integration logger: logger, helloTracker: null); - EspExitedEventArgs captured = null; + EspExitedEventArgs? captured = null; tracker.EspExited += (_, args) => captured = args; // Source time IS in the past relative to the clock — verify the tracker carries the diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleCollectorGuardTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleCollectorGuardTests.cs index bafd9e1c..ebc7873f 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleCollectorGuardTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleCollectorGuardTests.cs @@ -40,7 +40,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather try { Directory.Delete(_outsideUsers, recursive: true); } catch { /* best effort */ } } - private GatherRuleContext Context(bool unrestricted = false, string imeLogPathOverride = null) + private GatherRuleContext Context(bool unrestricted = false, string? imeLogPathOverride = null) => new GatherRuleContext( new AgentLogger(_tmp.Path, AgentLogLevel.Info), "sess", "tenant", @@ -52,13 +52,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather }; private static GatherRule Rule(string collectorType, string target, - Dictionary parameters = null) => new GatherRule + Dictionary? parameters = null) => new GatherRule { RuleId = "GATHER-GUARD-TEST", Title = "guard test", CollectorType = collectorType, Target = target, - Parameters = parameters, + // Match the model default (empty, never null) so the collectors see the + // same shape a deserialized rule without parameters gives them. + Parameters = parameters ?? new Dictionary(), Trigger = "startup", OutputEventType = "guard_test", }; diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorEmitModeTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorEmitModeTests.cs index 5955c8a9..dbd05631 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorEmitModeTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorEmitModeTests.cs @@ -192,7 +192,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather GatherRuleExecutor.ComputeCanonicalHash(new Dictionary { ["v"] = "1" }), GatherRuleExecutor.ComputeCanonicalHash(new Dictionary { ["v"] = 1 })); Assert.NotEqual( - GatherRuleExecutor.ComputeCanonicalHash(new Dictionary { ["v"] = null }), + GatherRuleExecutor.ComputeCanonicalHash(new Dictionary { ["v"] = null! }), GatherRuleExecutor.ComputeCanonicalHash(new Dictionary { ["v"] = "" })); } diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorPhaseExitTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorPhaseExitTests.cs index 94c467e6..41933f70 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorPhaseExitTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorPhaseExitTests.cs @@ -60,7 +60,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather } private static GatherRule ExitRule( - string id, string triggerPhase, List activePhases = null, string fromPhase = null) + string id, string triggerPhase, List? activePhases = null, string? fromPhase = null) => new GatherRule { RuleId = id, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorPhaseScopeTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorPhaseScopeTests.cs index ab96ac22..04eeb0ce 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorPhaseScopeTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Gather/GatherRuleExecutorPhaseScopeTests.cs @@ -63,7 +63,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather /// Interval rule with a 1h period — the timer never ticks within a test run, /// so these rules exist purely for synchronous scope-matrix assertions. - private static GatherRule ScopeProbeRule(string id, List activePhases = null, string fromPhase = null) + private static GatherRule ScopeProbeRule(string id, List? activePhases = null, string? fromPhase = null) => new GatherRule { RuleId = id, @@ -78,7 +78,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather ActiveFromPhase = fromPhase, }; - private static GatherRule StartupRule(string id, List activePhases = null, string fromPhase = null) + private static GatherRule StartupRule(string id, List? activePhases = null, string? fromPhase = null) => new GatherRule { RuleId = id, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Ime/CmTraceLogParserTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Ime/CmTraceLogParserTests.cs index 5af372fb..b1386ef0 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Ime/CmTraceLogParserTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Ime/CmTraceLogParserTests.cs @@ -27,7 +27,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Ime [InlineData(" ")] // whitespace only [InlineData("")] // leading junk (non-ignorable) fails the StartsWith gate - public void TryParseLine_returns_false_without_throwing_for_malformed_input(string line) + public void TryParseLine_returns_false_without_throwing_for_malformed_input(string? line) { var ok = CmTraceLogParser.TryParseLine(line, out var entry); diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Ime/ImeLogPatternReDoSTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Ime/ImeLogPatternReDoSTests.cs index 1d05718c..2ce2e4a3 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Ime/ImeLogPatternReDoSTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Ime/ImeLogPatternReDoSTests.cs @@ -24,12 +24,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Ime /// public sealed class ImeLogPatternReDoSTests { - private static ImeLogPattern Pattern(string id, string regex, string action = null) => new ImeLogPattern + private static ImeLogPattern Pattern(string id, string regex, string? action = null) => new ImeLogPattern { PatternId = id, Pattern = regex, Category = "always", - Action = action, + // Action is optional on the wire (absent = null); the model declares it non-nullable. + Action = action!, Enabled = true, Parameters = new Dictionary(), }; diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/AppxFailureAnalyzerTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/AppxFailureAnalyzerTests.cs index fa5e5c88..dc83a415 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/AppxFailureAnalyzerTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/AppxFailureAnalyzerTests.cs @@ -256,7 +256,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals [InlineData(null, false)] [InlineData("", false)] [InlineData("0x8007", false)] - public void IsAppxFamilyHresult_MatchesOnlyAppxDeploymentRange(string hresult, bool expected) + public void IsAppxFamilyHresult_MatchesOnlyAppxDeploymentRange(string? hresult, bool expected) { Assert.Equal(expected, AppxFailureAnalyzer.IsAppxFamilyHresult(hresult)); } diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/ProvisioningFailureSettleWindowTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/ProvisioningFailureSettleWindowTests.cs index f9cff16b..caed28e5 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/ProvisioningFailureSettleWindowTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/ProvisioningFailureSettleWindowTests.cs @@ -39,7 +39,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals public List EspFailures { get; } = new List(); public ProvisioningStatusTracker Tracker { get; } - public Fixture(Func> packageStatesProbe = null) + public Fixture(Func>? packageStatesProbe = null) { var clock = new VirtualClock(Fixed); var post = new InformationalEventPost(Sink, clock); @@ -179,7 +179,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals }"); var captured = f.CapturedEvents(); - Assert.Single(captured.Where(e => e.EventType == "esp_failure_settle_started")); + Assert.Single(captured, e => e.EventType == "esp_failure_settle_started"); } // ===================================================================== @@ -214,8 +214,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals // No terminal fire — the failure was retracted. Assert.Empty(f.EspFailures); - var recovered = Assert.Single(f.CapturedEvents() - .Where(e => e.EventType == "esp_failure_settle_recovered")); + var recovered = Assert.Single( + f.CapturedEvents(), e => e.EventType == "esp_failure_settle_recovered"); Assert.Equal("AccountSetup", (string)recovered.Data["category"]); Assert.Equal("Apps", (string)recovered.Data["failedSubcategory"]); Assert.Equal("inProgress", (string)recovered.Data["observedState"]); @@ -280,7 +280,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals f.Tracker.TriggerSettleTimerForTest("AccountSetupCategory.Status"); Assert.Single(f.EspFailures); - Assert.Empty(f.CapturedEvents().Where(e => e.EventType == "esp_failure_settle_recovered")); + Assert.DoesNotContain(f.CapturedEvents(), e => e.EventType == "esp_failure_settle_recovered"); } // ===================================================================== @@ -326,8 +326,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals ""AppsSubcategory"": {""subcategoryState"":""failed"",""subcategoryStatusText"":""Apps (Error)""} }"); - var settleStarted = Assert.Single(f.CapturedEvents() - .Where(e => e.EventType == "esp_failure_settle_started")); + var settleStarted = Assert.Single( + f.CapturedEvents(), e => e.EventType == "esp_failure_settle_started"); Assert.Equal(1, (int)settleStarted.Data["trackedAppsNotCompletedCount"]); var apps = (List>)settleStarted.Data["trackedAppsNotCompleted"]; var app = Assert.Single(apps); @@ -354,8 +354,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals ""SecurityPoliciesSubcategory"": {""subcategoryState"":""failed"",""subcategoryStatusText"":""Failed""} }"); - var settleStarted = Assert.Single(f.CapturedEvents() - .Where(e => e.EventType == "esp_failure_settle_started")); + var settleStarted = Assert.Single( + f.CapturedEvents(), e => e.EventType == "esp_failure_settle_started"); Assert.False(settleStarted.Data.ContainsKey("trackedAppsNotCompleted")); Assert.False(settleStarted.Data.ContainsKey("trackedAppsNotCompletedCount")); } diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/DecisionStepProcessorParkedTripwireTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/DecisionStepProcessorParkedTripwireTests.cs index 0fa609c2..a8f3fea2 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/DecisionStepProcessorParkedTripwireTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Orchestration/DecisionStepProcessorParkedTripwireTests.cs @@ -44,7 +44,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration private readonly FakeQuarantineSink _quarantine = new FakeQuarantineSink(); private readonly FakeSignalIngressSink _sink = new FakeSignalIngressSink(); private readonly DecisionEngine _engine = new DecisionEngine(); - private DecisionStepProcessor _sut; + private DecisionStepProcessor? _sut; public DecisionStepProcessorParkedTripwireTests() { @@ -107,7 +107,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration return (step, signal); } - private FakeSignalIngressSink.PostedSignal FindTripwirePost() + private FakeSignalIngressSink.PostedSignal? FindTripwirePost() { foreach (var posted in _sink.Posted) { @@ -121,6 +121,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration return null; } + /// The tripwire post, asserted present — for the assertions that only run + /// after WaitForTripwire() has already confirmed one was emitted. + private FakeSignalIngressSink.PostedSignal RequireTripwirePost() + { + var post = FindTripwirePost(); + Assert.NotNull(post); + return post!; + } + private int CountTripwirePosts() { var count = 0; @@ -165,9 +174,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration Thread.Sleep(SuppressWait); Assert.Equal(1, CountTripwirePosts()); - var post = FindTripwirePost(); + var post = RequireTripwirePost(); Assert.Equal(DecisionSignalKind.InformationalEvent, post.Kind); - Assert.Equal(nameof(SessionStage.EspAccountSetup), post.Payload["stage"]); + Assert.Equal(nameof(SessionStage.EspAccountSetup), post.Payload!["stage"]); Assert.Equal("Warning", post.Payload[SignalPayloadKeys.Severity]); Assert.Equal("true", post.Payload[SignalPayloadKeys.ImmediateUpload]); Assert.Contains("esp_final_exit", post.Payload["signalsSeen"]); @@ -222,7 +231,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration sut.ApplyStep(step, signal); Assert.True(WaitForTripwire()); - Assert.Equal(DeadlineNames.ClassifierTick, FindTripwirePost().Payload["armedDeadlines"]); + Assert.Equal(DeadlineNames.ClassifierTick, RequireTripwirePost().Payload!["armedDeadlines"]); } [Fact] @@ -237,7 +246,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration sut.ApplyStep(step, signal); Assert.True(WaitForTripwire()); - Assert.Equal(nameof(SessionStage.AwaitingDesktop), FindTripwirePost().Payload["stage"]); + Assert.Equal(nameof(SessionStage.AwaitingDesktop), RequireTripwirePost().Payload!["stage"]); } // ===================================================================== Dwell semantics diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Program/LifecycleEmittersRemoteConfigTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Program/LifecycleEmittersRemoteConfigTests.cs index 896bd3f2..ef5d78f6 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Program/LifecycleEmittersRemoteConfigTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Program/LifecycleEmittersRemoteConfigTests.cs @@ -256,8 +256,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program RemoteConfigFetchOutcome outcome, int configVersion = 0, int attempts = 1, - string failureType = null, - string failureMessage = null, + string? failureType = null, + string? failureMessage = null, int? authStatusCode = null) { // CurrentConfig is set via the protected SetConfig path — easiest: diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspFailureDetectedHResultTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspFailureDetectedHResultTests.cs index 67f36a06..0188588f 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspFailureDetectedHResultTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/EspFailureDetectedHResultTests.cs @@ -33,7 +33,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters [InlineData("Network connections (1 of 1 added)", null)] [InlineData("", null)] [InlineData(null, null)] - public void TryExtractErrorCode_extracts_parenthesised_hex_tail(string statusText, string expected) + public void TryExtractErrorCode_extracts_parenthesised_hex_tail(string? statusText, string? expected) { var actual = ProvisioningStatusTracker.TryExtractErrorCode(statusText); Assert.Equal(expected, actual); diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ImeLogTrackerAdapterTimingTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ImeLogTrackerAdapterTimingTests.cs index 3ed504f4..3e4b3658 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ImeLogTrackerAdapterTimingTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/ImeLogTrackerAdapterTimingTests.cs @@ -195,7 +195,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters var all = f.InfoEvents(SharedEventTypes.ScriptCompleted); Assert.Equal(2, all.Count); Assert.Equal("65.00", all[1].Payload!["durationSeconds"]); - Assert.Equal("cycle_including_reporting_latency", all[1].Payload["durationBasis"]); + Assert.Equal("cycle_including_reporting_latency", all[1].Payload!["durationBasis"]); } [Fact] diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Telemetry/BandwidthEstimatorTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Telemetry/BandwidthEstimatorTests.cs index ad2a7f90..bf870167 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Telemetry/BandwidthEstimatorTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Telemetry/BandwidthEstimatorTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic; using Xunit; @@ -19,7 +19,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Telemetry private static List Jobs(params BandwidthJobSample[] jobs) => new List(jobs); - private static BandwidthJobSample Job(string fileId, long wan, long lan = 0) + private static BandwidthJobSample Job(string? fileId, long wan, long lan = 0) => new BandwidthJobSample { FileId = fileId, WanBytes = wan, LanBytes = lan }; [Fact] @@ -43,8 +43,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Telemetry var estimate = estimator.TryBuildEstimate(); Assert.NotNull(estimate); Assert.Equal(1, estimate.WanSampleCount); - Assert.Equal(16.0, estimate.WanMbpsP90.Value, 3); - Assert.Equal(16.0, estimate.WanMbpsMax.Value, 3); + Assert.Equal(16.0, estimate.WanMbpsP90!.Value, 3); + Assert.Equal(16.0, estimate.WanMbpsMax!.Value, 3); Assert.Equal(6_000_000, estimate.WanBytesObserved); Assert.Equal("10-50", estimate.Bucket); } @@ -58,7 +58,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Telemetry estimator.AddSnapshot(T0.AddSeconds(3), Jobs(Job("a", 3_000_000), Job("b", 3_000_000))); var estimate = estimator.TryBuildEstimate(); - Assert.Equal(16.0, estimate.WanMbpsP90.Value, 3); + Assert.Equal(16.0, estimate.WanMbpsP90!.Value, 3); } [Fact] @@ -74,7 +74,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Telemetry var estimate = estimator.TryBuildEstimate(); Assert.Equal(2, estimate.WanSampleCount); - Assert.Equal(16.0, estimate.WanMbpsMax.Value, 3); + Assert.Equal(16.0, estimate.WanMbpsMax!.Value, 3); } [Fact] @@ -91,7 +91,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Telemetry estimator.AddSnapshot(T0.AddSeconds(63), Jobs(Job("a", 106_000_000))); var estimate = estimator.TryBuildEstimate(); Assert.Equal(1, estimate.WanSampleCount); - Assert.Equal(16.0, estimate.WanMbpsP90.Value, 3); + Assert.Equal(16.0, estimate.WanMbpsP90!.Value, 3); } [Fact] @@ -130,8 +130,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Telemetry estimator.AddSnapshot(T0.AddSeconds(3), Jobs(Job("a", wan: 6_000_000, lan: 375_000_000))); var estimate = estimator.TryBuildEstimate(); - Assert.Equal(16.0, estimate.WanMbpsP90.Value, 3); - Assert.Equal(1000.0, estimate.LanMbpsP90.Value, 3); + Assert.Equal(16.0, estimate.WanMbpsP90!.Value, 3); + Assert.Equal(1000.0, estimate.LanMbpsP90!.Value, 3); Assert.Equal("10-50", estimate.Bucket); // bucket keyed to WAN, not LAN } @@ -182,8 +182,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Telemetry var estimate = estimator.TryBuildEstimate(); Assert.Equal(20, estimate.WanSampleCount); - Assert.Equal(16.0, estimate.WanMbpsP90.Value, 3); // p90 stays on the steady plateau - Assert.Equal(160.0, estimate.WanMbpsMax.Value, 3); + Assert.Equal(16.0, estimate.WanMbpsP90!.Value, 3); // p90 stays on the steady plateau + Assert.Equal(160.0, estimate.WanMbpsMax!.Value, 3); } [Fact] @@ -265,7 +265,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Telemetry var estimate = estimator.TryBuildEstimate(); Assert.Equal(1, estimate.WanSampleCount); // only the finite positive sample survived - Assert.Equal(16.0, estimate.WanMbpsP90.Value, 3); + Assert.Equal(16.0, estimate.WanMbpsP90!.Value, 3); Assert.Equal(0, estimate.WanBytesObserved); } diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs index 17435b7f..44323339 100644 --- a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs +++ b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; +using System.Diagnostics.CodeAnalysis; using System.IdentityModel.Tokens.Jwt; using Microsoft.AspNetCore.Http; using System.Net; @@ -248,7 +249,7 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware /// missing/garbage tid here prevents an outbound OIDC-metadata fetch to a bogus authority on /// malformed-token floods. Loss-free — every legitimate Entra token carries a GUID tid. /// - internal static bool IsValidTenantId(string? tid) => Guid.TryParse(tid, out _); + internal static bool IsValidTenantId([NotNullWhen(true)] string? tid) => Guid.TryParse(tid, out _); /// /// Maps a token issuer + tenant id to the OIDC authority to fetch signing keys from. Entra v1.0 diff --git a/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs b/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs index 2fffee2a..a173415c 100644 --- a/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs +++ b/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs @@ -127,7 +127,7 @@ namespace AutopilotMonitor.Functions.Security // Parse certificate from Base64 var certBytes = Convert.FromBase64String(certificateBase64); - var certificate = new X509Certificate2(certBytes); + var certificate = X509CertificateLoader.LoadCertificate(certBytes); var thumbprint = certificate.Thumbprint; logger?.LogDebug($"Validating certificate: Subject={certificate.Subject}, Issuer={certificate.Issuer}, Thumbprint={thumbprint}"); diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index 96ee9533..baaa5d34 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -672,9 +672,11 @@ namespace AutopilotMonitor.Shared /// /// public static (string FailureType, string Message) ClassifyEspAppsFailure( - string errorCode, int? espTimeoutMinutes) + string? errorCode, int? espTimeoutMinutes) { - var normalised = string.IsNullOrEmpty(errorCode) ? null : errorCode.ToLowerInvariant(); + // netstandard2.0's IsNullOrEmpty lacks [NotNullWhen(false)], so the compiler can't + // see that the else branch is non-null (CS8602) — assert it. + var normalised = string.IsNullOrEmpty(errorCode) ? null : errorCode!.ToLowerInvariant(); if (string.Equals(normalised, DetectionFailureHResult, System.StringComparison.Ordinal)) { diff --git a/src/Shared/AutopilotMonitor.Shared/Services/AgentEndpointMigrationRules.cs b/src/Shared/AutopilotMonitor.Shared/Services/AgentEndpointMigrationRules.cs index ddf86b91..e0efd2f8 100644 --- a/src/Shared/AutopilotMonitor.Shared/Services/AgentEndpointMigrationRules.cs +++ b/src/Shared/AutopilotMonitor.Shared/Services/AgentEndpointMigrationRules.cs @@ -27,9 +27,11 @@ namespace AutopilotMonitor.Shared.Services /// Validates as a migration target and returns the /// normalized base URL (https://{lowercase-host}, no trailing slash) on success. /// Rules: absolute https URI, default port, no userinfo/path/query/fragment, host - /// matching . + /// matching . is non-null + /// exactly when this returns true (netstandard2.0 has no usable + /// [NotNullWhen], so callers must check the bool themselves). /// - public static bool TryNormalizeTarget(string candidate, out string normalized) + public static bool TryNormalizeTarget(string candidate, out string? normalized) { normalized = null; @@ -77,7 +79,8 @@ namespace AutopilotMonitor.Shared.Services /// differs from (both compared in normalized form — /// serving the agent its own current URL is a no-op, not a migration). /// - public static bool IsEffectiveMigration(string candidate, string currentBaseUrl, out string normalizedTarget) + public static bool IsEffectiveMigration( + string candidate, string currentBaseUrl, out string? normalizedTarget) { normalizedTarget = null;