Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - Add UI tests for No Access Page #593

Draft
wants to merge 4 commits into
base: add-no-access-page-feature
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using System.Text.Json;
using DfE.FindInformationAcademiesTrusts.Extensions;
using DfE.FindInformationAcademiesTrusts.Options;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;

Expand All @@ -13,9 +13,11 @@ public class AutomationAuthorizationHandler(
IWebHostEnvironment environment,
IHttpContextAccessor httpContextAccessor,
IOptions<TestOverrideOptions> testOverrideOptions)
: AuthorizationHandler<DenyAnonymousAuthorizationRequirement>,
IAuthorizationRequirement
: AuthorizationHandler<IAuthorizationRequirement>
{
private sealed record AutomationUserContext(string Name, string Email, string[] Roles);

private static readonly JsonSerializerOptions WebJsonSerializerOptions = new(JsonSerializerDefaults.Web);
private readonly string? _cypressTestSecret = testOverrideOptions.Value.CypressTestSecret;
private readonly bool _isLiveEnvironment = environment.IsLiveEnvironment();

Expand All @@ -35,26 +37,45 @@ public bool IsClientSecretHeaderValid()

public void SetupAutomationUser()
{
var httpContext = httpContextAccessor.HttpContext!;

var userContextJson = httpContext.Request.Headers["x-user-context"].ToString();
var automationUserContext =
JsonSerializer.Deserialize<AutomationUserContext>(userContextJson, WebJsonSerializerOptions);

if (automationUserContext == null)
throw new InvalidOperationException("Could not deserialize automation user context");

var identity = new ClaimsIdentity(new List<Claim>
{
new("name", "Automation User - name"),
new("preferred_username", "Automation User - email")
new("name", automationUserContext.Name),
new("preferred_username", automationUserContext.Email)
});
foreach (var role in automationUserContext.Roles)
identity.AddClaim(new Claim(ClaimTypes.Role, role));

var user = new ClaimsPrincipal(identity);

httpContextAccessor.HttpContext!.User = user;
httpContext.User = user;
}

[ExcludeFromCodeCoverage] // This method is difficult to test, everything that can be tested has been extracted to IsClientSecretHeaderValid
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
DenyAnonymousAuthorizationRequirement requirement)
[ExcludeFromCodeCoverage] // This method is difficult to test, everything that can be tested has been extracted to other public methods
public override Task HandleAsync(AuthorizationHandlerContext context)
{
if (IsClientSecretHeaderValid())
{
SetupAutomationUser();
context.Succeed(requirement);
foreach (var requirement in context.Requirements)
context.Succeed(requirement);
}

return Task.CompletedTask;
}

[ExcludeFromCodeCoverage] // This method is difficult to test because it is protected and not invoked
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
IAuthorizationRequirement requirement)
{
throw new NotImplementedException();
}
}
3 changes: 2 additions & 1 deletion DfE.FindInformationAcademiesTrusts/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,6 @@ private static void AddDependenciesTo(WebApplicationBuilder builder)
builder.Services.AddScoped<IAcademyService, AcademyService>();
builder.Services.AddScoped<IExportService, ExportService>();

builder.Services.AddScoped<IAuthorizationHandler, AutomationAuthorizationHandler>();
builder.Services.AddScoped<IOtherServicesLinkBuilder, OtherServicesLinkBuilder>();
builder.Services.AddScoped<IFreeSchoolMealsAverageProvider, FreeSchoolMealsAverageProvider>();
builder.Services.AddHttpContextAccessor();
Expand Down Expand Up @@ -276,6 +275,8 @@ private static void AddAuthenticationServices(WebApplicationBuilder builder)
});

builder.Services.AddAntiforgery(opts => { opts.Cookie.Name = FiatCookies.Antiforgery; });

builder.Services.AddScoped<IAuthorizationHandler, AutomationAuthorizationHandler>();
}

private static void AddDataProtectionServices(WebApplicationBuilder builder)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

export class AuthenticationInterceptor {

register(params?: AuthenticationInterceptorParams) {
register(automationUserProperties: AutomationUserProperties) {
cy.intercept(
{
url: Cypress.env("URL") + "/**",
Expand All @@ -11,14 +10,16 @@ export class AuthenticationInterceptor {
// Set an auth header on every request made by the browser
req.headers = {
...req.headers,
'Authorization': `Bearer ${Cypress.env("AUTH_KEY")}`
'Authorization': `Bearer ${Cypress.env("AUTH_KEY")}`,
'x-user-context': JSON.stringify(automationUserProperties)
};
}
).as("AuthInterceptor");
}
}

export type AuthenticationInterceptorParams = {
role?: string;
username?: string;
export type AutomationUserProperties = {
name: string;
email: string;
roles: string[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import navigation from "../../pages/navigation";

describe("Testing the components of the no access page", () => {

describe("As an unauthorised user", () => {

beforeEach(() => {
cy.login({
name: "Unauthorised User - name",
email: "Unauthorised User - email",
roles: []
});
});

['/', '/search', '/notfound', '/trusts/details?uid=5712', '/trusts/contacts?uid=5143', '/trusts/governance?5527'].forEach((url) => {
it(`Should redirect to no access page when accessing ${url}`, () => {
cy.visit(url);

navigation
.checkCurrentURLIsCorrect('no-access');

//todo: this test might get upset if the application redirects to microsoft login
// ideally we want to check that it tries to go to MS login and then redirects to no access page
// with the correct return url!
//This may not be possible though
});
});

['/cookies', '/accessibility', '/privacy'].forEach((url) => {
it(`Should be able to go to ${url}`, () => {
cy.visit(url);

navigation
.checkCurrentURLIsCorrect(url);
});
});
});

describe("As an authorised user", () => {
beforeEach(() => {
cy.login();
});

['/', '/search', '/notfound', '/trusts/details?uid=5712', '/trusts/contacts?uid=5143', '/trusts/governance?5527', '/cookies', '/accessibility', '/privacy'].forEach((url) => {
it(`Should be able to go to ${url}`, () => {
cy.visit(url);

navigation
.checkCurrentURLIsCorrect(url);
});
});
});

//Todo: Other tests - when not authenticated - no breadcrumb or search box or feedback footer section
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { AuthenticationInterceptor } from "../auth/authenticationInterceptor";
import { AuthenticationInterceptor, AutomationUserProperties } from "../auth/authenticationInterceptor";

Cypress.Commands.add("login", (params) => {
cy.clearCookies();
cy.clearLocalStorage();
Cypress.Commands.add("login", (automationUserProperties?: AutomationUserProperties) => {

if (!automationUserProperties)
automationUserProperties = {
name: "Automation User - name",
email: "Automation User - email",
roles: ["User.Role.Authorised"]
};

// Intercept all browser requests and add our special auth header
// Means we don't have to use azure to authenticate
new AuthenticationInterceptor().register(params);
new AuthenticationInterceptor().register(automationUserProperties);

cy.visit("/");
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AuthenticationInterceptorParams } from '../auth/authenticationInterceptor';
import { AutomationUserProperties } from '../auth/authenticationInterceptor';
import './commands'

declare global {
namespace Cypress {
interface Chainable {
login(params?: AuthenticationInterceptorParams): Chainable<Element>;
login(automationUserProperties?: AutomationUserProperties): Chainable<Element>;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,37 @@ public void ClientSecretHeaderValid_should_return_false_if_no_auth_header_provid
}

[Fact]
public void SetupAutomationUser_sets_correct_claims()
public void SetupAutomationUser_sets_claims_from_user_context_header_when_no_roles()
{
_httpContext.Request.Headers.Append("x-user-context",
"""{"name": "Unauthorised Automation User - name", "email": "Unauthorised Automation User - email", "roles": []}""");

_sut.SetupAutomationUser();
var expected = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
var actual = _httpContext.User;

// Exclude subject due to cyclical references
actual.Claims.Should().BeEquivalentTo(new Claim[]
{
new("name", "Automation User - name"),
new("preferred_username", "Automation User - email")
}));
new("name", "Unauthorised Automation User - name"),
new("preferred_username", "Unauthorised Automation User - email")
}, options => options.Excluding(claim => claim.Subject));
}

[Fact]
public void SetupAutomationUser_sets_claims_from_user_context_header_when_authorised_role()
{
_httpContext.Request.Headers.Append("x-user-context",
"""{"name": "Automation User - name", "email": "Automation User - email", "roles": ["User.Role.Authorised"]}""");

_sut.SetupAutomationUser();
var actual = _httpContext.User;

// Exclude subject due to cyclical references
actual.Claims.Should().BeEquivalentTo(expected.Claims, options => options.Excluding(claim => claim.Subject));
actual.Claims.Should().BeEquivalentTo(new Claim[]
{
new("name", "Automation User - name"),
new("preferred_username", "Automation User - email"),
new("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "User.Role.Authorised")
}, options => options.Excluding(claim => claim.Subject));
}
}
Loading