-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1215 from DFE-Digital/feature/readonly-transfer-p…
…rojects form a mat transfer projects sent to complete
- Loading branch information
Showing
24 changed files
with
579 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
Dfe.PrepareConversions/Dfe.PrepareConversions.Tests/Pages/BaseSecurityIntegrationTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} | ||
} |
63 changes: 63 additions & 0 deletions
63
Dfe.PrepareConversions/Dfe.PrepareConversions.Tests/Pages/SecurityTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."); | ||
|
||
} | ||
} | ||
} | ||
} |
121 changes: 121 additions & 0 deletions
121
...nversions/Dfe.PrepareConversions.Tests/SecurityIntegrationTestingWebApplicationFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} |
Oops, something went wrong.