btcpayserver-plugin-builder/PluginBuilder.Tests/PlaywrightTester.cs

239 lines
8.6 KiB
C#

using System.Security.Cryptography;
using Dapper;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Playwright;
using Newtonsoft.Json;
using Npgsql;
using PluginBuilder.Controllers.Logic;
using PluginBuilder.DataModels;
using PluginBuilder.Services;
using PluginBuilder.Util;
using PluginBuilder.Util.Extensions;
using Xunit;
namespace PluginBuilder.Tests;
public class PlaywrightTester : IAsyncDisposable
{
private string? CreatedUser;
public Uri? ServerUri;
public PlaywrightTester(XUnitLogger logger, ServerTester? server = null)
{
Logger = logger;
Server = server ?? new ServerTester("PlaywrightTest", logger);
}
public ServerTester Server { get; set; }
public IBrowser? Browser { get; private set; }
public IPage? Page { get; set; }
private XUnitLogger Logger { get; }
public string? Password { get; private set; }
public bool IsAdmin { get; private set; }
public async ValueTask DisposeAsync()
{
await SafeDispose(async () => await Page?.CloseAsync()!);
await SafeDispose(async () => await Browser?.CloseAsync()!);
await Server.DisposeAsync();
}
public async Task StartAsync()
{
await Server.Start();
var builder = new ConfigurationBuilder();
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
var playwright = await Playwright.CreateAsync();
Browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = true,
SlowMo = 0 // 50 if you want to slow down
});
var context = await Browser.NewContextAsync();
Page = await context.NewPageAsync();
Page.SetDefaultTimeout(10000); // Set default timeout to 10 seconds
ServerUri = new Uri(Server.WebApp.Urls.FirstOrDefault() ?? throw new InvalidOperationException("No URLs found"));
Logger.LogInformation($"Playwright: Using {Page.GetType()}");
Logger.LogInformation($"Playwright: Browsing to {ServerUri}");
await GoToLogin();
await AssertNoError();
}
private static async Task SafeDispose(Func<Task> action)
{
try
{
if (action != null)
await action();
}
catch { }
}
public async Task AssertNoError()
{
if (Page is null)
throw new InvalidOperationException("Page is not initialized");
var pageSource = await Page.ContentAsync();
if (pageSource.Contains("alert-danger"))
{
var dangerAlerts = Page.Locator(".alert-danger");
var count = await dangerAlerts.CountAsync();
for (var i = 0; i < count; i++)
{
var alert = dangerAlerts.Nth(i);
if (!await alert.IsVisibleAsync())
continue;
var alertText = await alert.InnerTextAsync();
Assert.Fail($"No alert should be displayed, but found this on {Page.Url}: {alertText}");
}
}
var errorPageMarker = Page.Locator("[data-testid='ui-error-page']");
if (await errorPageMarker.CountAsync() > 0)
{
var title = await Page.TitleAsync();
Assert.Fail($"Expected not to be on an error page, but found the UI error marker on {Page.Url} (title: {title})");
}
}
public async Task<string> CreateServerAdminAsync(string emailPrefix = "admin")
{
using var scope = Server.WebApp.Services.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
if (!await roleManager.RoleExistsAsync(Roles.ServerAdmin))
{
var roleCreateResult = await roleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
if (!roleCreateResult.Succeeded)
{
var errors = string.Join(", ", roleCreateResult.Errors.Select(e => e.Description));
throw new InvalidOperationException($"Failed to create server admin role: {errors}");
}
}
var email = $"{emailPrefix}-{Guid.NewGuid():N}@test.com";
const string password = "123456";
var user = new IdentityUser
{
UserName = email,
Email = email,
EmailConfirmed = true
};
var result = await userManager.CreateAsync(user, password);
if (!result.Succeeded)
{
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
throw new InvalidOperationException($"Failed to create admin user: {errors}");
}
var addToRoleResult = await userManager.AddToRoleAsync(user, Roles.ServerAdmin);
if (!addToRoleResult.Succeeded)
{
var errors = string.Join(", ", addToRoleResult.Errors.Select(e => e.Description));
throw new InvalidOperationException($"Failed to assign admin role to user {email}: {errors}");
}
return email;
}
public async Task EnableGithubVerificationAsync(NpgsqlConnection conn)
{
await conn.SettingsSetAsync(SettingsKeys.VerifiedGithub, "true");
var verificationCache = Server.GetService<AdminSettingsCache>();
await verificationCache.RefreshAllAdminSettings(conn);
}
public async Task<IResponse?> GoToUrl(string uri)
{
var fullUrl = new Uri(ServerUri ?? throw new InvalidOperationException(), uri).ToString();
return await Page?.GotoAsync(fullUrl, new PageGotoOptions { WaitUntil = WaitUntilState.Commit })!;
}
public async Task GoToLogin()
{
await GoToUrl("/login");
}
public async Task Logout()
{
await GoToUrl("/");
await Page?.Locator("#Nav-Account").ClickAsync()!;
await Page.Locator("#Nav-Logout").ClickAsync();
}
public static string GetRandomUInt256()
{
var bytes = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
}
public async Task<string> RegisterNewUser(bool isAdmin = false)
{
var usr = GetRandomUInt256()[(64 - 20)..] + "@a.com";
await Page?.FillAsync("#Email", usr)!;
await Page.FillAsync("#Password", "123456");
await Page.FillAsync("#ConfirmPassword", "123456");
if (isAdmin)
await Page.ClickAsync("#IsAdmin");
await Page.ClickAsync("#RegisterButton");
CreatedUser = usr;
Password = "123456";
IsAdmin = isAdmin;
return usr;
}
public async Task LogIn(string user, string password = "123456")
{
await GoToLogin();
await Page?.FillAsync("#Email", user)!;
await Page.FillAsync("#Password", password);
await Page.ClickAsync("#LoginButton");
}
public async Task VerifyUserAccounts(string email, string npub = "nostrNpub")
{
await using var scope = Server.WebApp.Services.CreateAsyncScope();
var factory = scope.ServiceProvider.GetRequiredService<DBConnectionFactory>();
await using var conn = await factory.Open();
var githubHandle = $"test-eligibility-{email.Replace("@", "_").Replace(".", "_")}";
var settings = new AccountSettings { Nostr = new NostrSettings { Npub = npub, Proof = "nostrProof" }, Github = githubHandle };
await conn.ExecuteAsync(
"""
UPDATE "AspNetUsers"
SET "EmailConfirmed" = TRUE,
"GithubGistUrl" = 'https://gist.github.com/test-eligibility',
"AccountDetail" = @AccountDetail::jsonb
WHERE "Email" = @Email;
""",
new { Email = email, AccountDetail = JsonConvert.SerializeObject(settings, CamelCaseSerializerSettings.Instance) });
}
public void CreateTestImage(string path)
{
var pngData = new byte[]
{
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4,
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41,
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
0x42, 0x60, 0x82
};
File.WriteAllBytes(path, pngData);
}
}