Skip to content

Commit

Permalink
Added unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
FrostyApeOne authored and FrostyApeOne committed Feb 18, 2025
1 parent 3da1db0 commit 860ee4b
Show file tree
Hide file tree
Showing 6 changed files with 573 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using AutoFixture;
using AutoFixture.AutoNSubstitute;
using DfE.CoreLibs.Security.Cypress;
using DfE.CoreLibs.Security.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;

namespace DfE.CoreLibs.Security.Tests.CypressTests
{
public class CypressAntiForgeryExtensionsTests
{
private readonly IFixture _fixture = new Fixture().Customize(new AutoNSubstituteCustomization());

[Fact]
public void AddCypressAntiForgeryHandling_RegistersRequiredServices()
{
// Arrange
var services = new ServiceCollection();
var mvcBuilder = Substitute.For<IMvcBuilder>();
mvcBuilder.Services.Returns(services);

// Act
var result = CypressAntiForgeryExtensions.AddCypressAntiForgeryHandling(mvcBuilder);

// Assert
Assert.Contains(services, d => d.ServiceType == typeof(ICypressRequestChecker) && d.ImplementationType == typeof(CypressRequestChecker));

Assert.Contains(services, d => d.ServiceType == typeof(CypressAwareAntiForgeryFilter));

Assert.Same(mvcBuilder, result);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using AutoFixture;
using AutoFixture.AutoNSubstitute;
using DfE.CoreLibs.Security.Cypress;
using DfE.CoreLibs.Security.Interfaces;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;

namespace DfE.CoreLibs.Security.Tests.CypressTests
{
public class CypressAuthenticationExtensionsTests
{
private readonly IFixture _fixture = new Fixture().Customize(new AutoNSubstituteCustomization());

[Fact]
public void AddCypressMultiAuthentication_RegistersRequiredServicesAndPolicyScheme()
{
// Arrange
var services = new ServiceCollection();

services.AddAuthentication();

var authBuilder = new AuthenticationBuilder(services);

// Act
authBuilder.AddCypressMultiAuthentication(policyScheme: "TestPolicy", cypressScheme: "CypressAuth", fallbackScheme: "Cookies");

Assert.Contains(services, d => d.ServiceType == typeof(ICypressRequestChecker) && d.ImplementationType == typeof(CypressRequestChecker));

var schemeProvider = services.BuildServiceProvider().GetService<IAuthenticationSchemeProvider>();
var scheme = schemeProvider.GetSchemeAsync("CypressAuth").Result;
Assert.NotNull(scheme);

// Create a fake HttpContext with required headers.
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers["x-user-context-name"] = "cypressUser";
httpContext.Request.Headers["Authorization"] = "Bearer secret123";

var checker = Substitute.For<ICypressRequestChecker>();
checker.IsCypressRequest(httpContext).Returns(true);

var sp = new ServiceCollection().AddSingleton(checker).BuildServiceProvider();
httpContext.RequestServices = sp;

var selector = new Func<HttpContext, string>(context =>
{
var chk = context.RequestServices.GetRequiredService<ICypressRequestChecker>();
return chk.IsCypressRequest(context) ? "CypressAuth" : "Cookies";
});
var schemeName = selector(httpContext);
Assert.Equal("CypressAuth", schemeName);
}

[Fact]
public void ForwardDefaultSelector_ReturnsCookies_WhenNotCypress()
{
// Arrange
const string cypressScheme = "CypressAuth";
const string fallbackScheme = CookieAuthenticationDefaults.AuthenticationScheme; // "Cookies"

var fakeChecker = Substitute.For<ICypressRequestChecker>();
fakeChecker.IsCypressRequest(Arg.Any<HttpContext>()).Returns(false);

var services = new ServiceCollection();
services.AddSingleton(fakeChecker);
var sp = services.BuildServiceProvider();

var httpContext = new DefaultHttpContext
{
RequestServices = sp
};

Func<HttpContext, string> forwardSelector = context =>
{
var checker = context.RequestServices.GetRequiredService<ICypressRequestChecker>();
return checker.IsCypressRequest(context) ? cypressScheme : fallbackScheme;
};

// Act
var selectedScheme = forwardSelector(httpContext);

// Assert
Assert.Equal(fallbackScheme, selectedScheme);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using AutoFixture;
using AutoFixture.AutoNSubstitute;
using DfE.CoreLibs.Security.Cypress;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using System.Security.Claims;

namespace DfE.CoreLibs.Security.Tests.CypressTests
{
public class CypressAuthenticationHandlerTests
{
private readonly IFixture _fixture = new Fixture().Customize(new AutoNSubstituteCustomization());

[Fact]
public async Task HandleAuthenticateAsync_ReturnsFail_WhenHttpContextIsNull()
{
// Arrange
var optionsMonitor = Substitute.For<IOptionsMonitor<AuthenticationSchemeOptions>>();
optionsMonitor.Get(Arg.Any<string>()).Returns(new AuthenticationSchemeOptions());
var loggerFactory = Substitute.For<ILoggerFactory>();
var encoder = Substitute.For<System.Text.Encodings.Web.UrlEncoder>();
var httpContextAccessor = Substitute.For<IHttpContextAccessor>();
httpContextAccessor.HttpContext.Returns((HttpContext)null);

var handler = new CypressAuthenticationHandler(optionsMonitor, loggerFactory, encoder, httpContextAccessor);

// Act
var result = await handler.CallBaseHandleAuthenticateAsync();

// Assert
Assert.False(result.Succeeded);
Assert.Contains("No HttpContext", result.Failure.Message);
}

[Fact]
public async Task HandleAuthenticateAsync_ReturnsSuccess_WithValidHeaders()
{
// Arrange
var optionsMonitor = Substitute.For<IOptionsMonitor<AuthenticationSchemeOptions>>();
optionsMonitor.Get(Arg.Any<string>()).Returns(new AuthenticationSchemeOptions());
var loggerFactory = Substitute.For<ILoggerFactory>();
var encoder = System.Text.Encodings.Web.UrlEncoder.Default;

var httpContext = new DefaultHttpContext();

httpContext.Request.Headers["x-user-context-id"] = "test-id";
httpContext.Request.Headers["x-user-context-name"] = "cypressUser";
httpContext.Request.Headers["x-user-context-role-0"] = "testRole";
httpContext.Request.Headers["Authorization"] = "Bearer secret123";

var httpContextAccessor = Substitute.For<IHttpContextAccessor>();
httpContextAccessor.HttpContext.Returns(httpContext);

var handler = new CypressAuthenticationHandler(optionsMonitor, loggerFactory, encoder, httpContextAccessor);

var scheme = new AuthenticationScheme("CypressAuth", "CypressAuth", typeof(CypressAuthenticationHandler));
await handler.InitializeAsync(scheme, httpContext);

var result = await handler.CallBaseHandleAuthenticateAsync();

// Assert
Assert.True(result.Succeeded);
var principal = result.Principal;
Assert.NotNull(principal);
var identity = principal.Identity as ClaimsIdentity;
Assert.NotNull(identity);

Assert.Contains(identity.Claims, c => c.Type == ClaimTypes.Name && c.Value == "cypressUser");
Assert.Contains(identity.Claims, c => c.Type == ClaimTypes.Role && c.Value == "testRole");
}

}

// Helper extension method to call protected HandleAuthenticateAsync.
public static class CypressAuthenticationHandlerTestExtensions
{
public static Task<AuthenticateResult> CallBaseHandleAuthenticateAsync(this CypressAuthenticationHandler handler)
{
// Use reflection to call the protected method.
var method = typeof(CypressAuthenticationHandler).GetMethod("HandleAuthenticateAsync", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
return (Task<AuthenticateResult>)method.Invoke(handler, null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using AutoFixture;
using AutoFixture.AutoNSubstitute;
using DfE.CoreLibs.Security.Cypress;
using DfE.CoreLibs.Security.Interfaces;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;

namespace DfE.CoreLibs.Security.Tests.CypressTests
{
public class CypressAwareAntiForgeryFilterTests
{
private readonly IFixture _fixture = new Fixture().Customize(new AutoNSubstituteCustomization());

private AuthorizationFilterContext CreateAuthorizationFilterContext(string method, string path = "/test")
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = method;
httpContext.Request.Path = path;

var routeData = new RouteData();
var actionDescriptor = new ActionDescriptor();
var modelState = new ModelStateDictionary();

var actionContext = new ActionContext(httpContext, routeData, actionDescriptor, modelState);
return new AuthorizationFilterContext(actionContext, new List<IFilterMetadata>());
}

[Fact]
public async Task OnAuthorizationAsync_Skips_When_ShouldSkipAntiforgeryPredicateReturnsTrue()
{
// Arrange
var antiforgery = Substitute.For<IAntiforgery>();
var logger = Substitute.For<ILogger<CypressAwareAntiForgeryFilter>>();
var cypressChecker = Substitute.For<ICypressRequestChecker>();
var options = Options.Create(new CypressAwareAntiForgeryOptions
{
ShouldSkipAntiforgery = _ => true
});
var filter = new CypressAwareAntiForgeryFilter(antiforgery, logger, cypressChecker, options);
var context = CreateAuthorizationFilterContext("POST");

// Act
await filter.OnAuthorizationAsync(context);

// Assert
await antiforgery.DidNotReceive().ValidateRequestAsync(context.HttpContext);
logger.Received().LogInformation("Skipping antiforgery due to ShouldSkipAntiforgery predicate.");
}

[Fact]
public async Task OnAuthorizationAsync_Skips_OnSafeHttpMethods()
{
// Arrange
var antiforgery = Substitute.For<IAntiforgery>();
var logger = Substitute.For<ILogger<CypressAwareAntiForgeryFilter>>();
var cypressChecker = Substitute.For<ICypressRequestChecker>();
var options = Options.Create(new CypressAwareAntiForgeryOptions
{
ShouldSkipAntiforgery = _ => false
});
var filter = new CypressAwareAntiForgeryFilter(antiforgery, logger, cypressChecker, options);
var context = CreateAuthorizationFilterContext("GET");

// Act
await filter.OnAuthorizationAsync(context);

// Assert
await antiforgery.DidNotReceive().ValidateRequestAsync(context.HttpContext);
}

[Fact]
public async Task OnAuthorizationAsync_Skips_ForCypressRequest()
{
// Arrange
var antiforgery = Substitute.For<IAntiforgery>();
var logger = Substitute.For<ILogger<CypressAwareAntiForgeryFilter>>();
var cypressChecker = Substitute.For<ICypressRequestChecker>();
cypressChecker.IsCypressRequest(Arg.Any<HttpContext>()).Returns(true);
var options = Options.Create(new CypressAwareAntiForgeryOptions
{
ShouldSkipAntiforgery = _ => false
});
var filter = new CypressAwareAntiForgeryFilter(antiforgery, logger, cypressChecker, options);
var context = CreateAuthorizationFilterContext("POST");

// Act
await filter.OnAuthorizationAsync(context);

// Assert
await antiforgery.DidNotReceive().ValidateRequestAsync(context.HttpContext);
logger.Received().LogInformation("Skipping antiforgery for Cypress request.");
}

[Fact]
public async Task OnAuthorizationAsync_EnforcesAntiforgery_ForNonCypressUnsafeRequest()
{
// Arrange
var antiforgery = Substitute.For<IAntiforgery>();
antiforgery.ValidateRequestAsync(Arg.Any<HttpContext>()).Returns(Task.CompletedTask);
var logger = Substitute.For<ILogger<CypressAwareAntiForgeryFilter>>();
var cypressChecker = Substitute.For<ICypressRequestChecker>();
cypressChecker.IsCypressRequest(Arg.Any<HttpContext>()).Returns(false);
var options = Options.Create(new CypressAwareAntiForgeryOptions
{
ShouldSkipAntiforgery = _ => false
});
var filter = new CypressAwareAntiForgeryFilter(antiforgery, logger, cypressChecker, options);
var context = CreateAuthorizationFilterContext("POST");

// Act
await filter.OnAuthorizationAsync(context);

// Assert
await antiforgery.Received().ValidateRequestAsync(context.HttpContext);
logger.Received().LogInformation("Enforcing antiforgery for non-Cypress request.");
}
}
}
Loading

0 comments on commit 860ee4b

Please sign in to comment.