Skip to content

Commit

Permalink
Merge pull request #1215 from DFE-Digital/feature/readonly-transfer-p…
Browse files Browse the repository at this point in the history
…rojects

form a mat transfer projects sent to complete
  • Loading branch information
paullocknimble authored Oct 31, 2024
2 parents 37bf679 + 2f919ed commit 0f59569
Show file tree
Hide file tree
Showing 24 changed files with 579 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public enum RoleCapability
CreateConversionProject,
CreateTransferProject,
DeleteConversionProject,
DeleteTransferProject
DeleteTransferProject,
AddIncomingTrustReferenceNumber
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Io;
using AngleSharp.Io.Network;
using AutoFixture;
using Dfe.PrepareConversions.Data.Features;
using DocumentFormat.OpenXml.Wordprocessing;
using FluentAssertions;
using Microsoft.FeatureManagement;
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace Dfe.PrepareConversions.Tests.Pages;

public abstract partial class BaseSecurityIntegrationTests : IClassFixture<SecurityIntegrationTestingWebApplicationFactory>, IDisposable
{
protected readonly SecurityIntegrationTestingWebApplicationFactory _factory;
protected readonly Fixture _fixture;
private readonly PathFor _pathFor;

protected BaseSecurityIntegrationTests(SecurityIntegrationTestingWebApplicationFactory factory)
{
_factory = factory;
_fixture = new Fixture();

Mock<IFeatureManager> featureManager = new();
featureManager.Setup(m => m.IsEnabledAsync("UseAcademisationApplication")).ReturnsAsync(true);
_pathFor = new PathFor(featureManager.Object);

Context = CreateBrowsingContext(factory.CreateClient());
}

protected IDocument Document => Context.Active;

protected IBrowsingContext Context { get; }


public void Dispose()
{
_factory.Reset();
GC.SuppressFinalize(this);
}

public static string BuildRequestAddress(string path)
{
return $"https://localhost{(path.StartsWith('/') ? path : $"/{path}")}";
}

protected async Task<IDocument> OpenAndConfirmPathAsync(string path, string expectedPath = null, string because = null)
{
var resultDocument = await Context.OpenAsync(BuildRequestAddress(path));

Document.Url.Should().Be(BuildRequestAddress(expectedPath ?? path), because ?? "navigation should be successful");
return resultDocument;
}

protected void VerifyElementDoesNotExist(string dataTest)
{
var anchors = Document.QuerySelectorAll($"[data-test='{dataTest}']").FirstOrDefault();
Assert.Null(anchors);
}

protected void VerifyNullElement(string linkText)
{
var anchors = Document.QuerySelectorAll("a");
var linkElement = anchors.SingleOrDefault(a => a.TextContent != null && a.TextContent.Contains(linkText));
Assert.Null(linkElement);
}

protected async Task NavigateAsync(string linkText, int? index = null)
{
IHtmlCollection<IElement> anchors = Document.QuerySelectorAll("a");
var link = (index == null
? anchors.Single(a => a.TextContent.Contains(linkText))
: anchors.Where(a => a.TextContent.Contains(linkText)).ElementAt(index.Value))
as IHtmlAnchorElement;

Assert.NotNull(link);
await link.NavigateAsync();
}

protected static Task<IDocument> NavigateAsync(IDocument document, string linkText, int? index = null)
{
IHtmlCollection<IElement> anchors = document.QuerySelectorAll("a");
IHtmlAnchorElement link = (index == null
? anchors.SingleOrDefault(a => a.TextContent.Contains(linkText))
: anchors.Where(a => a.TextContent.Contains(linkText)).ElementAt(index.Value))
as IHtmlAnchorElement;

if (index == null && link == null)
{
throw new NullReferenceException($"Sequence Contains no matching element. Could not find <a> element with text '{linkText}'");
}

Assert.NotNull(link);
return link.NavigateAsync();
}

protected async Task NavigateDataTestAsync(string dataTest)
{
IHtmlAnchorElement anchors = Document.QuerySelectorAll($"[data-test='{dataTest}']").First() as IHtmlAnchorElement;
Assert.NotNull(anchors);

await anchors.NavigateAsync();
}

protected void NavigateDataTestButton(string dataTest)
{
var buttons = Document.QuerySelectorAll($"[data-test='{dataTest}']").First() as IHtmlButtonElement;
Assert.NotNull(buttons);

buttons.DoClick();
}

protected IHtmlButtonElement GetDataTestButtonElement(string dataTest)
{
IHtmlButtonElement anchors = Document.QuerySelectorAll($"[data-test='{dataTest}']").FirstOrDefault() as IHtmlButtonElement;
Assert.NotNull(anchors);

return anchors;
}

protected static Task<IDocument> NavigateDataTestAsync(IDocument document, string dataTest)
{
IHtmlAnchorElement anchors = document.QuerySelectorAll($"[data-test='{dataTest}']").First() as IHtmlAnchorElement;
Assert.NotNull(anchors);

return anchors.NavigateAsync();
}
protected static void VerifyNullDataTest(IDocument document, string dataTest)
{
var anchor = document.QuerySelectorAll($"[data-test='{dataTest}']").FirstOrDefault();
Assert.Null(anchor);
}

protected static (RadioButton, RadioButton) RandomRadioButtons(string id, params string[] values)
{
Dictionary<int, string> keyPairs = values.Select((v, i) => new KeyValuePair<int, string>(i, v)).ToDictionary(kv => kv.Key + 1, kv => kv.Value);
int selectedPosition = new Random().Next(0, keyPairs.Count);
KeyValuePair<int, string> selected = keyPairs.ElementAt(selectedPosition);
keyPairs.Remove(selected.Key);
KeyValuePair<int, string> toSelect = keyPairs.ElementAt(new Random().Next(0, keyPairs.Count));
return (
new RadioButton { Id = Id(id, selected.Key), Value = selected.Value },
new RadioButton { Id = Id(id, toSelect.Key), Value = toSelect.Value });

static string Id(string name, int position)
{
return position == 1 ? $"#{name}" : $"#{name}-{position}";
}
}

private static IBrowsingContext CreateBrowsingContext(HttpClient httpClient)
{
IConfiguration config = AngleSharp.Configuration.Default
.WithRequester(new HttpClientRequester(httpClient))
.WithDefaultLoader(new LoaderOptions { IsResourceLoadingEnabled = true });

return BrowsingContext.New(config);
}

protected IEnumerable<IElement> ElementsWithText(string tag, string content)
{
return Document.QuerySelectorAll(tag).Where(t => t.TextContent.Contains(content));
}

protected IHtmlInputElement InputWithId(string inputId)
{
IHtmlInputElement inputElement = Document.QuerySelector<IHtmlInputElement>(inputId.StartsWith('#') ? inputId : $"#{inputId}");
inputElement.Should().NotBeNull(because: $"element with ID {inputId} should be available on the page ({Document.Url})");
return inputElement;
}

protected async Task ClickCommonSubmitButtonAsync()
{
IHtmlButtonElement buttonElement = Document.QuerySelector<IHtmlButtonElement>("button[data-cy=\"select-common-submitbutton\"]");

buttonElement.Should()
.NotBeNull(because: $"A button with the common submit button selector (data-cy=\"select-common-submitbutton\") is expected on this page ({Document.Url})");

await buttonElement!.SubmitAsync();
}

protected static string CypressSelectorFor(string name)
{
return $"[data-cy='{name}']";
}

protected class RadioButton
{
public string Value { get; init; }
public string Id { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using FluentAssertions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Dfe.PrepareConversions.Tests.Pages;

public class SecurityTests : BaseSecurityIntegrationTests
{
public SecurityTests(SecurityIntegrationTestingWebApplicationFactory factory) : base(factory) { }

[Fact]
public async Task Check_Auth()
{
var _anonymousPages = new[] { "/Diagnostics", "/public/maintenance", "/public/accessibility", "/public/cookie-preferences", "/access-denied" };
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});

// Act

var _endpointSources = _factory.Services.GetService<IEnumerable<EndpointDataSource>>();

var endpoints = _endpointSources
.SelectMany(es => es.Endpoints)
.OfType<RouteEndpoint>()
.Where(es =>
!es.RoutePattern.RawText.Contains("MicrosoftIdentity") &&
!es.RoutePattern.RawText.Equals("/") &&
!es.Metadata.Any(m => m is RouteNameMetadata && ((RouteNameMetadata)m).RouteName == "default"));


foreach (var endpoint in endpoints)
{
var route = "/" + endpoint.RoutePattern.RawText.Replace("Index", "").Trim('/');

var isAnonymousPage = _anonymousPages.Contains(route);

var hasAuthorizeMetadata = endpoint.Metadata.Any(m => m is AuthorizeAttribute);
var hasAnonymousMetadata = endpoint.Metadata.Any(m => m is AllowAnonymousAttribute);
var authorizeAttribute = endpoint.Metadata.OfType<AuthorizeAttribute>().FirstOrDefault();

if (isAnonymousPage)
{
Assert.True(hasAnonymousMetadata, $"Page {route} should be anonymous.");
}
else
{

Assert.True(hasAuthorizeMetadata, $"Page {route} should be protected.");

}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Dfe.PrepareConversions.Data.Services.Interfaces;
using Dfe.PrepareConversions.Tests.Pages.ProjectAssignment;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.FeatureManagement;
using Moq;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net.Http;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using WireMock.Logging;
using WireMock.Matchers;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
using WireMock.Util;
using Xunit.Abstractions;

// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedMember.Global

namespace Dfe.PrepareConversions.Tests;

public class SecurityIntegrationTestingWebApplicationFactory : WebApplicationFactory<Startup>
{
private static int _currentPort = 5080;
private static readonly object Sync = new();

private readonly WireMockServer _mockApiServer;

public SecurityIntegrationTestingWebApplicationFactory()
{
int port = AllocateNext();
_mockApiServer = WireMockServer.Start(port);
_mockApiServer.LogEntriesChanged += EntriesChanged;
}

public ITestOutputHelper DebugOutput { get; set; }

public IUserRepository UserRepository { get; private set; }

public IEnumerable<LogEntry> GetMockServerLogs(string path, HttpMethod verb = null)
{
IRequestBuilder requestBuilder = Request.Create().WithPath(path);
if (verb is not null) requestBuilder.UsingMethod(verb.Method);
return _mockApiServer.FindLogEntries(requestBuilder);
}

private void EntriesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
DebugOutput?.WriteLine($"API Server change: {JsonConvert.SerializeObject(e)}");
}

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config =>
{
string projectDir = Directory.GetCurrentDirectory();
string configPath = Path.Combine(projectDir, "appsettings.json");

config.Sources.Clear();
config
.AddJsonFile(configPath)
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "TramsApi:Endpoint", _mockApiServer.Url },
{ "AcademisationApi:BaseUrl", _mockApiServer.Url },
{ "AzureAd:AllowedRoles", string.Empty }, // Do not restrict access for integration test
{ "ServiceLink:TransfersUrl", "https://an-external-service.com/" }
})
.AddEnvironmentVariables();
});

Mock<IFeatureManager> featureManager = new();
featureManager.Setup(m => m.IsEnabledAsync("UseAcademisationApplication")).ReturnsAsync(true);
featureManager.Setup(m => m.IsEnabledAsync("ShowDirectedAcademyOrders")).ReturnsAsync(true);

UserRepository = new TestUserRepository();

builder.ConfigureServices(services =>
{
services.AddScoped(x => UserRepository);
services.AddTransient(_ => featureManager.Object);
});
}

private static int AllocateNext()
{
lock (Sync)
{
int next = _currentPort;
_currentPort++;
return next;
}
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (disposing)
{
_mockApiServer.Stop();
}
}
public void Reset()
{
_mockApiServer.Reset();
}

}
Loading

0 comments on commit 0f59569

Please sign in to comment.