Skip to content

Commit

Permalink
Feature playwright tests cleanup (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
CZEMacLeod authored Aug 15, 2023
1 parent a14db9b commit 79def04
Show file tree
Hide file tree
Showing 19 changed files with 530 additions and 630 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
timeout-minutes: 10
container:
image: mcr.microsoft.com/playwright/dotnet:v1.35.0-jammy
options: --ipc=host
steps:
- uses: actions/checkout@v3
- name: Setup dotnet
Expand All @@ -50,4 +51,6 @@ jobs:
dotnet-version: 7.0.x
- run: dotnet build
- name: Execute Playwright tests
env:
TestHostStartDelay: 1000
run: dotnet test --no-build
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,9 @@ FodyWeavers.xsd
.idea/**/dictionaries
.idea/**/shelf
.idea/**/contentModel.xml
.idea/httpRequests
.idea/httpRequests

# SQLite
*.db
*.db-shm
*.db-wal
12 changes: 7 additions & 5 deletions src/TagzApp.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using TagzApp.Web.Data;
using Microsoft.AspNetCore.Authorization;

namespace TagzApp.Web;

Expand All @@ -14,17 +13,19 @@ private static void Main(string[] args)
{

var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("SecurityContextConnection") ?? throw new InvalidOperationException("Connection string 'SecurityContextConnection' not found.");

builder.Services.AddDbContext<SecurityContext>(options => options.UseSqlite(connectionString));
// Late bind the connection string so that any changes to the configuration made later on, or in the test fixture can be picked up.
builder.Services.AddDbContext<SecurityContext>((services,options) =>
options.UseSqlite(services.GetRequiredService<IConfiguration>().GetConnectionString("SecurityContextConnection") ??
throw new InvalidOperationException("Connection string 'SecurityContextConnection' not found.")));

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true
)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<SecurityContext>();

_ = builder.Services.AddAuthentication().AddExtenalProviders(builder.Configuration);
_ = builder.Services.AddAuthentication().AddExternalProviders(builder.Configuration);

builder.Services.AddAuthorization(config =>
{
Expand All @@ -44,6 +45,7 @@ private static void Main(string[] args)
options.Conventions.AuthorizePage("/Moderation", Security.Policy.Moderator);
});


builder.Services.AddTagzAppHostedServices(builder.Configuration);

builder.Services.AddSignalR();
Expand Down Expand Up @@ -84,7 +86,7 @@ private static void Main(string[] args)

}

builder.InitializeSecurity(app.Services);
app.Services.InitializeSecurity().GetAwaiter().GetResult(); // Ensure this runs before we start the app.

app.Run();

Expand Down
100 changes: 53 additions & 47 deletions src/TagzApp.Web/ServicesExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using TagzApp.Web;
using TagzApp.Web.Data;
using TagzApp.Web.Services;

public static class ServicesExtensions {
namespace TagzApp.Web;

public static class ServicesExtensions
{

public static IServiceCollection ConfigureProvider<T>(this IServiceCollection services, IConfiguration configuration) where T : IConfigureProvider, new()
{
Expand Down Expand Up @@ -55,70 +56,75 @@ public static IServiceCollection AddTagzAppHostedServices(this IServiceCollectio
/// A collection of externally configured providers
/// </summary>
public static List<IConfigureProvider> SocialMediaProviders { get; set; } = new();

public static AuthenticationBuilder AddExtenalProviders(this AuthenticationBuilder builder, IConfiguration configuration)
{

if (!string.IsNullOrEmpty(configuration["Authentication:Microsoft:ClientId"]))
public static AuthenticationBuilder AddExternalProvider(this AuthenticationBuilder builder, string name, IConfiguration configuration,
Action<IConfiguration> action)
{
var section = configuration.GetSection($"Authentication:{name}");
if (section is not null) action(section);
return builder;
}

builder.AddMicrosoftAccount(microsoftOptions =>
public static AuthenticationBuilder AddExternalProvider(this AuthenticationBuilder builder, string name, IConfiguration configuration,
Action<string, string> action)
{
microsoftOptions.ClientId = configuration["Authentication:Microsoft:ClientId"]!;
microsoftOptions.ClientSecret = configuration["Authentication:Microsoft:ClientSecret"]!;
return builder.AddExternalProvider(name, configuration, (section) => {
var clientID = section["ClientID"];
var clientSecret = section["ClientSecret"];
if (!string.IsNullOrEmpty(clientID) && !string.IsNullOrEmpty(clientSecret))
{
action(clientID, clientSecret);
}
});

}

if (!string.IsNullOrEmpty(configuration["Authentication:GitHub:ClientId"]))
public static AuthenticationBuilder AddExternalProvider(this AuthenticationBuilder builder, string name, IConfiguration configuration,
Action<Action<Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions>> action)
{

builder.AddGitHub(ghOptions =>
return builder.AddExternalProvider(name, configuration, (section) => {
var clientID = section["ClientID"];
var clientSecret = section["ClientSecret"];
if (!string.IsNullOrEmpty(clientID) && !string.IsNullOrEmpty(clientSecret))
{
action(options =>
{
ghOptions.ClientId = configuration["Authentication:GitHub:ClientId"]!;
ghOptions.ClientSecret = configuration["Authentication:GitHub:ClientSecret"]!;
});

}

if (!string.IsNullOrEmpty(configuration["Authentication:LinkedIn:ClientId"]))
options.ClientId = clientID;
options.ClientSecret = clientSecret;
});
}
});
}

public static AuthenticationBuilder AddExternalProviders(this AuthenticationBuilder builder, IConfiguration configuration)
{

builder.AddLinkedIn(liOptions =>
{
liOptions.ClientId = configuration["Authentication:LinkedIn:ClientId"]!;
liOptions.ClientSecret = configuration["Authentication:LinkedIn:ClientSecret"]!;
});

}
builder.AddExternalProvider("Microsoft", configuration , options => builder.AddMicrosoftAccount(options));
builder.AddExternalProvider("GitHub", configuration, options => builder.AddGitHub(options));
builder.AddExternalProvider("LinkedIn", configuration, options => builder.AddLinkedIn(options));

return builder;

}

public static async Task InitializeSecurity(this WebApplicationBuilder builder, IServiceProvider services)
public static async Task InitializeSecurity(this IServiceProvider services)
{

using (var scope = services.CreateScope())
{
using var scope = services.CreateScope();

// create database if not exists
var dbContext = services.GetRequiredService<SecurityContext>();
await dbContext.Database.EnsureCreatedAsync();
// create database if not exists
var dbContext = scope.ServiceProvider.GetRequiredService<SecurityContext>();
await dbContext.Database.EnsureCreatedAsync();

var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
if (!(await roleManager.RoleExistsAsync(Security.Role.Admin)))
{
await roleManager.CreateAsync(new IdentityRole(Security.Role.Admin));
}
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
if (!(await roleManager.RoleExistsAsync(Security.Role.Admin)))
{
await roleManager.CreateAsync(new IdentityRole(Security.Role.Admin));
}

if (!(await roleManager.RoleExistsAsync(Security.Role.Moderator)))
{
await roleManager.CreateAsync(new IdentityRole(Security.Role.Moderator));
}
if (!(await roleManager.RoleExistsAsync(Security.Role.Moderator)))
{
await roleManager.CreateAsync(new IdentityRole(Security.Role.Moderator));
}

}

}
}

}
17 changes: 0 additions & 17 deletions src/TagzApp.WebTest/BaseFixture.cs

This file was deleted.

61 changes: 61 additions & 0 deletions src/TagzApp.WebTest/Fixtures/FixtureExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
namespace TagzApp.WebTest.Fixtures;

public static class FixtureExtensions
{
public static IHostBuilder UseUniqueDb(this IHostBuilder builder, Guid id) =>
builder.ConfigureAppConfiguration(configuration =>
{
var testConfiguration = new Dictionary<string, string?>()
{
{"ConnectionStrings:SecurityContextConnection",$"Data Source=TagzApp.Web.{id:N}.db" }
};
configuration.AddInMemoryCollection(testConfiguration);
});

public static async Task CleanUpDbFilesAsync(this Guid id, ILogger? logger = null)
{
logger ??= Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
// The host should have shutdown here so we can delete the test database files
await Task.Delay(50);
var dbFiles = System.IO.Directory.GetFiles(".", $"TagzApp.Web.{id:N}.db*");
foreach (var dbFile in dbFiles)
{
try
{
logger.LogInformation("Removing test database file {File}", dbFile);
System.IO.File.Delete(dbFile);
}
catch (Exception e)
{
logger.LogWarning("Could not remove test database file {File}: {Reason}", dbFile, e.Message);
}
}
}

/// <summary>
/// Add the file provided from the test project to the host app configuration
/// </summary>
/// <param name="builder">The IHostBuilder</param>
/// <param name="fileName">The filename or null (defaults to appsettings.Test.json)</param>
/// <returns>Returns the IHostBuilder to allow chaining</returns>
public static IHostBuilder AddTestConfiguration(this IHostBuilder builder, string? fileName = null)
{
var testDirectory = System.IO.Directory.GetCurrentDirectory();
builder.ConfigureAppConfiguration(host => host.AddJsonFile(System.IO.Path.Combine(testDirectory, fileName ?? "appsettings.Test.json"), true));
return builder;
}

/// <summary>
/// Applies a startup delay based on the configuration parameter TestHostStartDelay. This allows easy adding of a custom delay on build / test servers.
/// </summary>
/// <param name="serviceProvider">The IServiceProvider used to get the IConfiguration</param>
/// <remarks>The default delay if no value is found is 0 and no delay is applied.</remarks>
public static async Task ApplyStartUpDelay(this IServiceProvider serviceProvider)
{
var config = serviceProvider.GetRequiredService<IConfiguration>();
if (int.TryParse(config["TestHostStartDelay"] ?? "0", out var delay) && delay != 0)
{
await Task.Delay(delay);
}
}
}
48 changes: 48 additions & 0 deletions src/TagzApp.WebTest/Fixtures/PlaywrightFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using C3D.Extensions.Playwright.AspNetCore.Xunit;

namespace TagzApp.WebTest.Fixtures;

/// <summary>
/// WebApplicationFactory that wraps the TestHost in a Kestrel server and provides Playwright and HttpClient testing.
/// This also logs output from the Host under test to Xunit.
///
/// <p>
/// Credit to <a href="https://github.com/CZEMacLeod">https://github.com/CZEMacLeod</a> for writing this.
/// Functionality is now wrapped in the nuget package C3D.Extensions.Playwright.AspNetCore.Xunit
/// </p>
/// </summary>
public class PlaywrightFixture : PlaywrightFixture<Web.Program>
{
public override string? Environment { get; } = "Development";

public PlaywrightFixture(IMessageSink output) : base(output) { }

private readonly Guid _Uniqueid = Guid.NewGuid();

protected override IHost CreateHost(IHostBuilder builder)
{
//ServicesExtensions.SocialMediaProviders = new List<IConfigureProvider> { new StartStubSocialMediaProvider() };
builder.AddTestConfiguration();
builder.UseOnlyStubSocialMediaProvider();
builder.UseUniqueDb(_Uniqueid);
var host = base.CreateHost(builder);

return host;
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Base class calls SuppressFinalize")]
public async override ValueTask DisposeAsync()
{
await base.DisposeAsync();

var logger = this.MessageSink.CreateLogger<PlaywrightFixture>();
await _Uniqueid.CleanUpDbFilesAsync(logger);
}

// Temp hack to see if it is a timing issue in github actions
public async override Task InitializeAsync()
{
await base.InitializeAsync();
await Services.ApplyStartUpDelay();
}
}
Loading

0 comments on commit 79def04

Please sign in to comment.