Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions.Tests/GraphAuthFailureTests.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

100 lines
4.1 KiB
C#

using System.Net;
using AutopilotMonitor.Functions.Security;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared.DataAccess;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Pins the shared 401/403 contract of the Graph-backed device validators: attempt 1 drops the
/// tenant's cached token (it may predate the consent grant on this instance) and reports
/// transient so the loop re-mints; attempt 2 and every other status leave the token alone.
/// </summary>
public class GraphAuthFailureTests
{
private const string TenantId = "11111111-1111-1111-1111-111111111111";
private static Mock<GraphTokenService> TokenService()
{
var cache = new MemoryCache(new MemoryCacheOptions());
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["EntraId:ClientId"] = "aaaaaaaa-0000-0000-0000-000000000001",
["EntraId:ClientSecret"] = "secret",
}).Build();
var registry = new EntraAppRegistry(configuration, NullLogger<EntraAppRegistry>.Instance);
var tenantConfig = new Mock<TenantConfigurationService>(
Mock.Of<IConfigRepository>(), NullLogger<TenantConfigurationService>.Instance, cache)
{ CallBase = false };
return new Mock<GraphTokenService>(
NullLogger<GraphTokenService>.Instance, Mock.Of<IHttpClientFactory>(), cache, registry, tenantConfig.Object)
{ CallBase = false };
}
[Theory]
[InlineData(HttpStatusCode.Unauthorized)]
[InlineData(HttpStatusCode.Forbidden)]
public void AuthFailure_OnFirstAttempt_InvalidatesOnce_AndAsksForRetry(HttpStatusCode status)
{
var tokens = TokenService();
var retry = GraphAuthFailure.TryRecoverStaleToken(tokens.Object, NullLogger.Instance, "Test", TenantId, status, attempt: 1);
Assert.True(retry);
tokens.Verify(t => t.InvalidateTenant(TenantId), Times.Once);
}
[Theory]
[InlineData(HttpStatusCode.Unauthorized)]
[InlineData(HttpStatusCode.Forbidden)]
public void AuthFailure_OnSecondAttempt_LeavesTheFreshToken_AndDefersToTheValidator(HttpStatusCode status)
{
var tokens = TokenService();
var retry = GraphAuthFailure.TryRecoverStaleToken(tokens.Object, NullLogger.Instance, "Test", TenantId, status, attempt: 2);
Assert.False(retry);
tokens.Verify(t => t.InvalidateTenant(It.IsAny<string>()), Times.Never);
}
[Theory]
[InlineData(HttpStatusCode.NotFound)]
[InlineData(HttpStatusCode.BadRequest)]
[InlineData(HttpStatusCode.TooManyRequests)]
[InlineData(HttpStatusCode.InternalServerError)]
[InlineData(HttpStatusCode.GatewayTimeout)]
public void OtherStatuses_NeverTouchTheToken(HttpStatusCode status)
{
var tokens = TokenService();
var retry = GraphAuthFailure.TryRecoverStaleToken(tokens.Object, NullLogger.Instance, "Test", TenantId, status, attempt: 1);
Assert.False(retry);
Assert.False(GraphAuthFailure.IsAuthFailure(status));
tokens.Verify(t => t.InvalidateTenant(It.IsAny<string>()), Times.Never);
}
[Fact]
public void PermissionMissingRetryAfter_IsLongerThanTheDefault_ButInsideTheAgentsPatience()
{
// 30 s is the SecurityValidator default for a transient failure; a consent gap needs more
// than that to close, and the agent's replay logic honours any Retry-After.
Assert.True(GraphAuthFailure.PermissionMissingRetryAfterSeconds > 30);
Assert.True(GraphAuthFailure.PermissionMissingRetryAfterSeconds <= 300);
}
[Theory]
[InlineData(null, null, null)]
[InlineData(null, 120, 120)]
[InlineData(30, null, 30)]
[InlineData(30, 120, 120)]
[InlineData(120, 30, 120)]
public void SecurityValidator_KeepsTheLargestRetryAfter(int? accumulated, int? next, int? expected)
=> Assert.Equal(expected, SecurityValidator.MaxRetryAfter(accumulated, next));
}