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

OSOE-365: Support for running tests without a browser #396

Merged
merged 26 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
432b1a4
Disable the default search engine selector splash screen
Piedone Aug 2, 2024
77cd46e
Adding the ability to launch tests without a browser
Piedone Aug 2, 2024
c7127be
Revert "Adding the ability to launch tests without a browser"
Piedone Aug 2, 2024
773f0a0
Making browser initialization on-demand
Piedone Aug 2, 2024
3da1e43
Temporarily disabling WebDriver-using code for security scans
Piedone Aug 2, 2024
d496ecf
Re-adding Browser.None and making it possible to use a browser for se…
Piedone Aug 3, 2024
c61908a
Security scanning tests now don't use a browser
Piedone Aug 3, 2024
dc2f785
Renaming IsBrowserUsed
Piedone Aug 3, 2024
e4c739c
Not using a browser TestRunTimeoutShouldThrowAsync
Piedone Aug 3, 2024
1730abd
Fixing that UsingScopeAsync() didn't work with no-browser tests
Piedone Aug 3, 2024
71947fe
Adding more ExecuteTestAfterBrowserSetupWithoutBrowserAsync overloads
Piedone Aug 3, 2024
94c1ff2
Merge remote-tracking branch 'origin/dev' into issue/OSOE-365
Piedone Aug 3, 2024
2e2251f
IsBrowserRunning now can be flipped back too
Piedone Aug 3, 2024
cecd400
Docs
Piedone Aug 3, 2024
0231bdf
Fixing failure dump generation when not running a browser
Piedone Aug 3, 2024
1b7deaa
Disabling Application Error Disclosure security scan alerts for the S…
Piedone Aug 3, 2024
566aaaa
YAML code styling
Piedone Aug 3, 2024
6146ba9
Refactoring
Piedone Aug 3, 2024
5449a79
Fixing screenshot dump generation
Piedone Aug 3, 2024
1e72359
Docs
Piedone Aug 3, 2024
6b212a0
Code styling
Piedone Aug 3, 2024
007c5be
Fixing SecurityScanningTests sample
Piedone Aug 3, 2024
a902fe7
Code styling
Piedone Aug 4, 2024
8ce4b54
Surfacing the Uri that the setup operation returns as the StartUri
Piedone Aug 4, 2024
adb45ec
Fixing TestStartUri handling
Piedone Aug 4, 2024
cfb8312
Merge remote-tracking branch 'origin/dev' into issue/OSOE-365
Piedone Aug 5, 2024
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
11 changes: 7 additions & 4 deletions Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper)
// will fail the scan, but don't worry! You'll get a nice report about the findings in the failure dump.
[Fact]
public Task BasicSecurityScanShouldPass() =>
ExecuteTestAfterSetupAsync(
// Note how we use a method that doesn't launch a browser. Security scanning happens fully in ZAP, and doesn't
// use the browser launched by the UI Testing Toolbox. Not starting a browser for the test makes it a bit
// faster. However, you can opt to launch a browser to prepare the app for security scanning if necessary.
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
context => context.RunAndAssertBaselineSecurityScanAsync(),
// You should configure the assertion that checks the app logs to accept some common cases that only should
// appear during security scanning. If you launch a full scan, this is automatically configured by the
Expand All @@ -74,15 +77,15 @@ public Task BasicSecurityScanShouldPass() =>
// are only present to illustrate the type of adjustments you may want for your own site.
[Fact]
public Task SecurityScanWithCustomConfigurationShouldPass() =>
ExecuteTestAfterSetupAsync(
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
context => context.RunAndAssertBaselineSecurityScanAsync(
configuration => configuration
////.UseAjaxSpider() // This is quite slow so just showing you here but not running it.
.ExcludeUrlWithRegex(".*blog.*")
.DisablePassiveScanRule(10020, "The response does not include either Content-Security-Policy with 'frame-ancestors' directive.")
.DisableScanRuleForUrlWithRegex(".*/about", 10038, "Content Security Policy (CSP) Header Not Set")
.SignIn(),
sarifLog => sarifLog.Runs[0].Results.Count.ShouldBe(1)),
sarifLog => sarifLog.Runs[0].Results.Count.ShouldBe(0)),
changeConfiguration: configuration => configuration.UseAssertAppLogsForSecurityScan());

// Let's get low-level into ZAP's configuration now. While the .NET configuration API of the Lombiq UI Testing
Expand All @@ -105,7 +108,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass() =>
// customize them if something you need is not surfaced as configuration.
[Fact]
public Task SecurityScanWithCustomAutomationFrameworkPlanShouldPass() =>
ExecuteTestAfterSetupAsync(
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
context => context.RunAndAssertSecurityScanAsync(
"Tests/CustomZapAutomationFrameworkPlan.yml",
configuration => configuration
Expand Down
2 changes: 1 addition & 1 deletion Lombiq.Tests.UI.Tests.UI/TestCases/TimeoutTestCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public static class TimeoutTestCases
{
public static Task TestRunTimeoutShouldThrowAsync(
ExecuteTestAfterSetupAsync executeTestAfterSetupAsync,
Browser browser = default) =>
Browser browser = Browser.None) =>
Should.ThrowAsync(
async () => await executeTestAfterSetupAsync(
context => Task.Delay(TimeSpan.FromSeconds(1)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ public static async Task UsingScopeAsync(

try
{
// If there's no Default shell settings then the shell host hasn't been initialized yet. This can happen if
// no request hit the app yet.
var defaultShellSettingExist = shellHost.TryGetSettings("Default", out var _);

if (!defaultShellSettingExist)
{
await shellHost.InitializeAsync();
}

// Injecting a fake HttpContext is required for many things, but it needs to happen before UsingAsync()
// below to avoid NullReferenceExceptions in
// OrchardCore.Recipes.Services.RecipeEnvironmentFeatureProvider.PopulateEnvironmentAsync. Migrations
Expand Down
23 changes: 13 additions & 10 deletions Lombiq.Tests.UI/Extensions/VerificationUITestContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static async Task AssertLogsAsync(this UITestContext context)
var configuration = context.Configuration;
var testOutputHelper = configuration.TestOutputHelper;

await context.UpdateHistoricBrowserLogAsync();
if (context.IsBrowserRunning) await context.UpdateHistoricBrowserLogAsync();

try
{
Expand All @@ -34,16 +34,19 @@ public static async Task AssertLogsAsync(this UITestContext context)
throw;
}

try
{
configuration.AssertBrowserLog?.Invoke(context.HistoricBrowserLog);
}
catch (Exception)
if (context.IsBrowserRunning)
{
testOutputHelper.WriteLine("Browser logs: " + Environment.NewLine);
testOutputHelper.WriteLine(context.HistoricBrowserLog.ToFormattedString());

throw;
try
{
configuration.AssertBrowserLog?.Invoke(context.HistoricBrowserLog);
}
catch (Exception)
{
testOutputHelper.WriteLine("Browser logs: " + Environment.NewLine);
testOutputHelper.WriteLine(context.HistoricBrowserLog.ToFormattedString());

throw;
}
}
}
}
47 changes: 47 additions & 0 deletions Lombiq.Tests.UI/OrchardCoreUITestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,53 @@ protected virtual Task ExecuteMultiSizeTestAfterSetupAsync(
browser,
changeConfigurationAsync);

protected virtual Task ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Action<OrchardCoreUITestExecutorConfiguration> changeConfiguration = null) =>
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(testAsync, default, changeConfiguration);

protected Task ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Func<OrchardCoreUITestExecutorConfiguration, Task> changeConfigurationAsync) =>
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(testAsync, default, changeConfigurationAsync);

protected virtual Task ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Browser browser,
Action<OrchardCoreUITestExecutorConfiguration> changeConfiguration = null) =>
ExecuteTestAfterBrowserSetupWithoutBrowserAsync(testAsync, changeConfiguration.AsCompletedTask());

protected Task ExecuteTestAfterBrowserSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Browser browser,
Func<OrchardCoreUITestExecutorConfiguration, Task> changeConfigurationAsync) =>
ExecuteTestAfterSetupWithoutBrowserAsync(testAsync, async configuration =>
{
configuration.SetupConfiguration.BeforeSetup = configuration =>
{
configuration.BrowserConfiguration.Browser = browser;
return Task.CompletedTask;
};

configuration.SetupConfiguration.AfterSetup = configuration =>
{
configuration.BrowserConfiguration.Browser = Browser.None;
return Task.CompletedTask;
};

if (changeConfigurationAsync != null) await changeConfigurationAsync(configuration);
});

protected virtual Task ExecuteTestAfterSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Action<OrchardCoreUITestExecutorConfiguration> changeConfiguration = null) =>
ExecuteTestAfterSetupWithoutBrowserAsync(testAsync, changeConfiguration.AsCompletedTask());

protected Task ExecuteTestAfterSetupWithoutBrowserAsync(
Func<UITestContext, Task> testAsync,
Func<OrchardCoreUITestExecutorConfiguration, Task> changeConfigurationAsync) =>
ExecuteTestAfterSetupAsync(testAsync, Browser.None, changeConfigurationAsync);

protected virtual Task ExecuteTestAfterSetupAsync(
Action<UITestContext> test,
Action<OrchardCoreUITestExecutorConfiguration> changeConfiguration = null) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ jobs:
threshold: high
name: passiveScan-config
type: passiveScan-config
- alertFilters:
# The Error shortcut shows an error page by design.
- ruleId: 90022
ruleName: Application Error Disclosure (90022)
context: ''
newRisk: False Positive
parameter: ''
parameterRegex: false
url: .*/Lombiq.Tests.UI.Shortcuts/Error/Index
urlRegex: true
attack: ''
attackRegex: false
evidence: ''
evidenceRegex: false
methods: []
parameters:
deleteGlobalAlerts: false
name: alertFilter
type: alertFilter
- parameters: {}
name: spider
type: spider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ jobs:
evidence: ''
evidenceRegex: false
methods: []
# The Error shortcut shows an error page by design.
- ruleId: 90022
ruleName: Application Error Disclosure (90022)
context: ''
newRisk: False Positive
parameter: ''
parameterRegex: false
url: .*/Lombiq.Tests.UI.Shortcuts/Error/Index
urlRegex: true
attack: ''
attackRegex: false
evidence: ''
evidenceRegex: false
methods: []
parameters:
deleteGlobalAlerts: false
name: alertFilter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ public static Task<SecurityScanResult> RunSecurityScanAsync(
Action<SecurityScanConfiguration> configure = null)
{
var configuration = new SecurityScanConfiguration()
.StartAtUri(context.GetCurrentUri());
.StartAtUri(context.IsBrowserRunning ? context.GetCurrentUri() : context.TestStartUri);

// By default ignore /vendor/ or /vendors/ URLs. This is case-insensitive. We have no control over them, and
// they may contain several false positives (e.g. in font-awesome).
Expand Down
28 changes: 18 additions & 10 deletions Lombiq.Tests.UI/Services/AtataFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public static async Task<AtataScope> StartAtataScopeAsync(
var browserConfiguration = configuration.BrowserConfiguration;

var builder = AtataContext.Configure()
.UseDriver(await CreateDriverAsync(browserConfiguration, timeoutConfiguration, testOutputHelper))
.UseBaseUrl(baseUri.ToString())
.UseCulture(browserConfiguration.AcceptLanguage.ToString())
.UseTestName(configuration.AtataConfiguration.TestName)
Expand All @@ -42,6 +41,17 @@ public static async Task<AtataScope> StartAtataScopeAsync(
.PageSnapshots.UseCdpOrPageSourceStrategy() // #spell-check-ignore-line
.UseArtifactsPathTemplate(contextId); // Necessary to prevent long paths, an issue under Windows.

if (configuration.BrowserConfiguration.Browser != Browser.None)
{
builder
.UseDriverInitializationStage(AtataContextDriverInitializationStage.OnDemand)
.UseDriver(await CreateDriverFactoryAsync(browserConfiguration, timeoutConfiguration, testOutputHelper));
}
else
{
builder.UseDriverInitializationStage(AtataContextDriverInitializationStage.None);
}

builder.LogConsumers.AddDebug();
builder.LogConsumers.Add(new TestOutputLogConsumer(testOutputHelper));

Expand All @@ -55,15 +65,11 @@ public static void SetupShellCliCommandFactory() =>
.UseCmdForWindows()
.UseForOtherOS(new BashShellCliCommandFactory("-login"));

private static async Task<IWebDriver> CreateDriverAsync(
private static async Task<Func<IWebDriver>> CreateDriverFactoryAsync(
BrowserConfiguration browserConfiguration,
TimeoutConfiguration timeoutConfiguration,
ITestOutputHelper testOutputHelper)
{
Task<T> FromAsync<T>(Func<BrowserConfiguration, TimeSpan, Task<T>> factory)
where T : IWebDriver =>
factory(browserConfiguration, timeoutConfiguration.PageLoadTimeout);

// Driver creation can fail with "Cannot start the driver service on http://localhost:56686/" exceptions if the
// machine is under load. Retrying it here so not the whole test needs to be re-run.
const int maxTryCount = 3;
Expand All @@ -79,12 +85,14 @@ Task<T> FromAsync<T>(Func<BrowserConfiguration, TimeSpan, Task<T>> factory)
{
try
{
var pageLoadTimeout = timeoutConfiguration.PageLoadTimeout;

return browserConfiguration.Browser switch
{
Browser.Chrome => await FromAsync(WebDriverFactory.CreateChromeDriverAsync),
Browser.Edge => await FromAsync(WebDriverFactory.CreateEdgeDriverAsync),
Browser.Firefox => await FromAsync(WebDriverFactory.CreateFirefoxDriverAsync),
Browser.InternetExplorer => await FromAsync(WebDriverFactory.CreateInternetExplorerDriverAsync),
Browser.Chrome => await WebDriverFactory.CreateChromeDriverAsync(browserConfiguration, pageLoadTimeout),
Browser.Edge => await WebDriverFactory.CreateEdgeDriverAsync(browserConfiguration, pageLoadTimeout),
Browser.Firefox => await WebDriverFactory.CreateFirefoxDriverAsync(browserConfiguration, pageLoadTimeout),
Browser.InternetExplorer => await WebDriverFactory.CreateInternetExplorerDriverAsync(browserConfiguration, pageLoadTimeout),
_ => throw new InvalidOperationException($"Unknown browser: {browserConfiguration.Browser}."),
};
}
Expand Down
13 changes: 12 additions & 1 deletion Lombiq.Tests.UI/Services/AtataScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ public sealed class AtataScope : IDisposable
private Uri _baseUri;

public AtataContext AtataContext { get; }
public IWebDriver Driver => AtataContext.Driver;

public IWebDriver Driver
{
get
{
var driver = AtataContext.Driver;
IsBrowserRunning = driver != null;
return driver;
}
}

public bool IsBrowserRunning { get; private set; }

public Uri BaseUri
{
Expand Down
2 changes: 2 additions & 0 deletions Lombiq.Tests.UI/Services/OrchardCoreSetupConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace Lombiq.Tests.UI.Services;

public delegate Task BeforeSetupHandler(OrchardCoreUITestExecutorConfiguration configuration);
public delegate Task AfterSetupHandler(OrchardCoreUITestExecutorConfiguration configuration);

/// <summary>
/// Configuration for the initial setup of an Orchard Core app.
Expand Down Expand Up @@ -35,4 +36,5 @@ public class OrchardCoreSetupConfiguration
Path.Combine(DirectoryPaths.Temp, DirectoryPaths.SetupSnapshot);

public BeforeSetupHandler BeforeSetup { get; set; }
public AfterSetupHandler AfterSetup { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public enum Browser
Edge,
Firefox,
InternetExplorer,

/// <summary>
/// No browser will be used. Useful for testing things that don't require a browser, like API endpoints or running
/// security scans.
/// </summary>
None,
}

public class OrchardCoreUITestExecutorConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Lombiq.Tests.UI.Services;

public delegate Task<(UITestContext Context, Uri ResultUri)> AppInitializer();
public delegate Task<(UITestContext Context, Uri TestStartUri)> AppInitializer();

/// <summary>
/// Service for transparently running operations on a web application and snapshotting them just a single time, so the
Expand All @@ -25,7 +25,7 @@ public class SynchronizingWebApplicationSnapshotManager
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly string _snapshotDirectoryPath;

private Uri _resultUri;
private Uri _testStartUri;
private bool _snapshotCreated;

public SynchronizingWebApplicationSnapshotManager(string snapshotDirectoryPath) => _snapshotDirectoryPath = snapshotDirectoryPath;
Expand All @@ -37,7 +37,7 @@ public async Task<Uri> RunOperationAndSnapshotIfNewAsync(AppInitializer appIniti
await _semaphore.WaitAsync();
try
{
if (_snapshotCreated) return _resultUri;
if (_snapshotCreated) return _testStartUri;

DebugHelper.WriteLineTimestamped("Creating snapshot.");

Expand All @@ -53,7 +53,7 @@ public async Task<Uri> RunOperationAndSnapshotIfNewAsync(AppInitializer appIniti
// At the end so if any exception happens above then it won' be mistakenly set to true.
_snapshotCreated = true;

return _resultUri ??= result.ResultUri;
return _testStartUri ??= result.TestStartUri;
}
finally
{
Expand Down
Loading