Skip to content

Commit

Permalink
Fix avoid blocked ports (#14)
Browse files Browse the repository at this point in the history
* Add editor config

* Add code to avoid blocked ports and allow overriding the port.

* Add code for firefox and chromium to override the blocked ports list.

* Add tests to check that firefox and chromium can access a blocked port.
Also tests that webkit can't access the port.
  • Loading branch information
CZEMacLeod authored Aug 21, 2023
1 parent f93ff8c commit 4280f91
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 10 deletions.
93 changes: 93 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
[*.cs]

dotnet_diagnostic.CS0169.severity = warning
dotnet_diagnostic.CS0414.severity = warning

csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_throw_expression = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_indent_labels = one_less_than_current
csharp_style_prefer_index_operator = true:suggestion

[*.{cs,vb}]
#### Naming styles ####

# Naming rules

dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i

dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case

dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case

# Symbol specifications

dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =

dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =

dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =

# Naming styles

dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case

dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf
1 change: 1 addition & 0 deletions C3D.Extensions.Playwright.AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "C3D.Extensions.Playwright.A
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{DA726315-C6FB-4BCB-A766-4BD32A9643A2}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
global.json = global.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Playwright;
using System.Diagnostics.CodeAnalysis;

Expand All @@ -22,9 +23,26 @@ public class PlaywrightWebApplicationFactory<TProgram> : WebApplicationFactory<T
private int? port;
// We randomize the server port so we ensure that any hard coded Uri's fail in the tests.
// This also allows multiple servers to run during the tests.
public int Port => port ??= 5000 + Interlocked.Add(ref nextPort, 10 + System.Random.Shared.Next(10));
public virtual int Port => port ??= GetNextPort();
public string Uri => uri ??= $"http://localhost:{Port}";

private static readonly int[] avoidPorts = new[] {
5060, 5061,
6000,
6566,
6665, 6666, 6667, 6668, 6669,
6697, 10080 };

private int GetNextPort()
{
var port = 5000 + Interlocked.Add(ref nextPort, 10 + System.Random.Shared.Next(10));
while (avoidPorts.Contains(port)) // We shouldn't ever roll into the next port because we have a space of at least 10 ports
{
port++;
}
return port;
}

private static int nextPort = 0;

#region "Overridable Properties"
Expand Down Expand Up @@ -123,7 +141,7 @@ public async Task<IBrowser> CreateCustomPlaywrightBrowserAsync(PlaywrightBrowser
EnsureBrowserInstalled(browserType);
var launchOptions = new BrowserTypeLaunchOptions(LaunchOptions);
browserOptions?.Invoke(launchOptions);
return await LaunchAsync(GetBrowser(browserType), launchOptions);
return await LaunchBrowserAsync(GetBrowser(browserType), launchOptions);
}

private void EnsureBrowserInstalled(PlaywrightBrowserType? browserType)
Expand Down Expand Up @@ -205,25 +223,85 @@ public virtual async Task InitializeAsync()
PlaywrightUtilities.InstallPlaywright(BrowserType);
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
playwright ??= (await Microsoft.Playwright.Playwright.CreateAsync()) ?? throw new InvalidOperationException();
browser ??= (await LaunchAsync(GetBrowser(), LaunchOptions)) ?? throw new InvalidOperationException();
browser ??= (await LaunchBrowserAsync(GetBrowser(), LaunchOptions)) ?? throw new InvalidOperationException();
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
}

private async Task<IBrowser> LaunchAsync(IBrowserType browser, BrowserTypeLaunchOptions options)
private async Task<IBrowser> LaunchBrowserAsync(IBrowserType browser, BrowserTypeLaunchOptions options)
{
List<string> args = new(options.Args ?? Enumerable.Empty<string>());

// In most browsers, ports are blocked, such as sip ports 5060/5061.
// As these may be picked, we explicitly allow whatever port this host is running on.

switch (browser.Name)
{
case "chromium":
options = UpdateChromiumOptions(options, args);
break;
case "firefox":
options = UpdateFirefoxOptions(options);

break;
}
return await browser.LaunchAsync(options);
}

private BrowserTypeLaunchOptions UpdateFirefoxOptions(BrowserTypeLaunchOptions options)
{
var prefs = options.FirefoxUserPrefs is null ? new Dictionary<string, object>() : new Dictionary<string, object>(options.FirefoxUserPrefs);
if (prefs.TryGetValue("network.security.ports.banned.override", out var portsList))
{
var ports = ((string)portsList).Split(',').Select(p => int.Parse(p)).ToList();
if (!ports.Contains(Port))
{
ports.Add(Port);
prefs["network.security.ports.banned.override"] = string.Join(',', ports);
options = new(options)
{
FirefoxUserPrefs = prefs
};
}
}
else
{
prefs["network.security.ports.banned.override"] = Port.ToString();
options = new(options)
{
FirefoxUserPrefs = prefs
};
}

return options;
}

private BrowserTypeLaunchOptions UpdateChromiumOptions(BrowserTypeLaunchOptions options, List<string> args)
{
var args = (options.Args ?? Array.Empty<string>()).ToList();
var ports = args.SingleOrDefault(arg => arg.StartsWith("--explicitly-allowed-ports"));
if (ports is null && browser.Name == "chromium")
var portsArg = args.SingleOrDefault(arg => arg.StartsWith("--explicitly-allowed-ports"));
if (portsArg is null)
{
// In chromium some ports are blocked, such as sip ports 5060/5061.
// As these may be picked, we explicitly allow whatever port this host is running on.
args.Add($"--explicitly-allowed-ports={Port}");
options = new(options)
{
Args = args
};
}
return await browser.LaunchAsync(options);
else
{
var ports = portsArg.Split('=', 2)[1].Split(',').Select(p => int.Parse(p)).ToList();
if (!ports.Contains(Port))
{
ports.Add(Port);
args.Remove(portsArg);
args.Add($"--explicitly-allowed-ports={string.Join(',', ports)}");
options = new(options)
{
Args = args
};
}
}

return options;
}

protected override void Dispose(bool disposing)
Expand Down
90 changes: 90 additions & 0 deletions test/Sample.WebApp.Tests/BlockedPortsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using C3D.Extensions.Playwright.AspNetCore;
using C3D.Extensions.Playwright.AspNetCore.Xunit;
using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Xunit.Abstractions;

namespace Sample.WebApp.Tests;

public class Port5060Fixture : PlaywrightFixture<Program>
{
public Port5060Fixture(IMessageSink output) : base(output) { }

public override int Port => 5060; // Force the application to port 5060.
}

public class BlockedPortsTests : IClassFixture<Port5060Fixture>
{
private readonly PlaywrightFixture<Program> webApplication;
private readonly ITestOutputHelper outputHelper;

public BlockedPortsTests(Port5060Fixture webApplication, ITestOutputHelper outputHelper)
{
this.webApplication = webApplication;
this.outputHelper = outputHelper;
}

private void WriteFunctionName([CallerMemberName] string? caller = null) => outputHelper.WriteLine(caller);


[Fact]
public async Task ChromiumAllowsBlockedPort()
{
WriteFunctionName();

var page = await webApplication.CreatePlaywrightPageAsync();
await page.GotoAsync("/");

var pageTitle = await page.TitleAsync();
var uri = page.Url;
var webAppUri = new Uri(webApplication.Uri).ToString(); // Note, this will add the trailing / to the Uri

await page.CloseAsync();

Assert.Equal(webAppUri, uri); // Check browser goes to expected page
Assert.Equal("Home page", pageTitle); // Check browser can read title
}


[Fact]
public async Task FireFoxAllowsBlockedPort()
{
WriteFunctionName();

await using var browserPage = await webApplication.CreateCustomPlaywrightBrowserPageAsync(PlaywrightBrowserType.Firefox);
var page = browserPage.Page; // This is for convienence only
await page.GotoAsync("/");

var pageTitle = await page.TitleAsync();
var uri = page.Url;
var webAppUri = new Uri(webApplication.Uri).ToString(); // Note, this will add the trailing / to the Uri
// We don't need to close the page here as the browserPage utility object will do it for us.

Assert.Equal(webAppUri, uri); // Check browser goes to expected page
Assert.Equal("Home page", pageTitle); // Check browser can read title
}

[Fact]
public async Task WebkitDoesNotAllowBlockedPort()
{
WriteFunctionName();

await using var browserPage = await webApplication.CreateCustomPlaywrightBrowserPageAsync(PlaywrightBrowserType.Webkit);
var page = browserPage.Page; // This is for convienence only
await page.GotoAsync("/");

var pageTitle = await page.TitleAsync();
var uri = page.Url;
var webAppUri = new Uri(webApplication.Uri).ToString(); // Note, this will add the trailing / to the Uri
// We don't need to close the page here as the browserPage utility object will do it for us.

Assert.NotEqual(webAppUri, uri); // Check browser *didn't* go to expected page
Assert.NotEqual("Home page", pageTitle); // Check browser *didn't* read title
}

}

0 comments on commit 4280f91

Please sign in to comment.