Files
decePubClient/tests/e2e/Support/Browser.cs
T

101 lines
3.5 KiB
C#
Raw Normal View History

using Microsoft.Playwright;
[assembly: AssemblyFixture(typeof(decePubClient.E2E.Support.Browser))]
namespace decePubClient.E2E.Support
{
// One Chromium for the whole run. *.test names go to Caddy on 127.0.0.1:6443 (Chromium's own --resolve), whose
// certificates come from the pasture's private CA, so certificate errors are ignored; nothing else is reachable.
public sealed class Browser : IAsyncLifetime
{
IPlaywright _playwright;
public IBrowser Chromium { get; private set; }
public async ValueTask InitializeAsync()
{
_playwright = await Playwright.CreateAsync();
Chromium = await _playwright.Chromium.LaunchAsync(new()
{
Headless = Environment.GetEnvironmentVariable("E2E_HEADED") != "1",
Args = ["--host-resolver-rules=MAP *.test 127.0.0.1:6443"]
});
}
public async ValueTask DisposeAsync()
{
if (Chromium is not null)
await Chromium.CloseAsync();
_playwright?.Dispose();
}
public async Task<Session> Open(string storageState = null)
{
var context = await Chromium.NewContextAsync(new()
{
IgnoreHTTPSErrors = true,
Locale = "en-GB",
TimezoneId = "Europe/Rome",
ViewportSize = new() { Width = 1440, Height = 900 },
ServiceWorkers = ServiceWorkerPolicy.Block,
StorageStatePath = storageState
});
return new Session(context, await context.NewPageAsync());
}
}
// A browser tab and what it did: every request's host, every console error, every 5xx and Blazor's error bar. The
// guards hold for every test: the app talks only to its own site and to PrivaPub, and never fails silently.
public sealed class Session : IAsyncDisposable
{
public IBrowserContext Context { get; }
public IPage Page { get; }
public List<string> Hosts { get; } = [];
public List<string> ConsoleErrors { get; } = [];
public List<string> ServerErrors { get; } = [];
public List<(string Url, string Authorization)> ApiCalls { get; } = [];
public Session(IBrowserContext context, IPage page)
{
Context = context;
Page = page;
page.Request += (_, request) =>
{
var uri = new Uri(request.Url);
if (uri.Scheme is "http" or "https")
lock (Hosts)
Hosts.Add(uri.Host);
if (uri.Host == "privapub.test")
lock (ApiCalls)
ApiCalls.Add((request.Url, request.Headers.TryGetValue("authorization", out var auth) ? auth : null));
};
page.Console += (_, message) =>
{
if (message.Type == "error")
lock (ConsoleErrors)
ConsoleErrors.Add(message.Text);
};
page.Response += (_, response) =>
{
if (response.Status >= 500)
lock (ServerErrors)
ServerErrors.Add($"{response.Status} {response.Url}");
};
}
public async Task AssertGuards()
{
Assert.All(Hosts.Distinct(), host => Assert.Contains(host, new[] { "decepub.test", "privapub.test" }));
Assert.Empty(ServerErrors);
Assert.False(await Page.Locator("#blazor-error-ui").IsVisibleAsync(), "Blazor's error bar is showing");
// /clientapi carries the root's JWT and /api a persona's token: no credential ever crosses over
var clientApi = ApiCalls.Where(c => c.Authorization is not null && new Uri(c.Url).AbsolutePath.StartsWith("/clientapi/"))
.Select(c => c.Authorization).ToHashSet();
var mastodonApi = ApiCalls.Where(c => c.Authorization is not null && new Uri(c.Url).AbsolutePath.StartsWith("/api/"))
.Select(c => c.Authorization).ToHashSet();
Assert.Empty(clientApi.Intersect(mastodonApi));
Assert.All(clientApi, auth => Assert.Equal(3, auth["Bearer ".Length..].Split('.').Length));
}
public async ValueTask DisposeAsync() => await Context.CloseAsync();
}
}