mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
98d87a8579
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>
100 lines
4.1 KiB
C#
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));
|
|
}
|