Ships the E2E infrastructure filed against task #199 (UnsTab drag-drop Playwright smoke). The Blazor Server interactive-render assertion through a test-owned pipeline needs a dedicated diagnosis pass — filed as task #242 — but the Playwright harness lands here so that follow-up starts from a known-good scaffolding rather than setting up the project from scratch. ## New project tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests - AdminWebAppFactory — boots the Admin pipeline with Kestrel on a free loopback port, swaps the SQL DbContext for EF Core InMemory, replaces the LDAP cookie auth with TestAuthHandler, mirrors the Razor-components/auth/antiforgery pipeline, and seeds a cluster + draft generation with areas warsaw / berlin and a line-a1 in warsaw. Not a WebApplicationFactory<Program> because WAF's TestServer transport doesn't coexist cleanly with Kestrel-on-a-real-port, which Playwright needs. - TestAuthHandler — stamps every request with a FleetAdmin claim so tests hit authenticated routes without the LDAP bind. - PlaywrightFixture — one Chromium launch shared across tests; throws PlaywrightBrowserMissingException when the binary isn't installed so tests can Assert.Skip rather than fail hard. - UnsTabDragDropE2ETests.Admin_host_serves_HTTP_via_Playwright_scaffolding — proves the full stack comes up: Kestrel bind, InMemory DbContext, test auth, Playwright navigation, Razor route pipeline responds with HTML < 500. One passing test. ## Prerequisite Chromium must be installed locally: pwsh tests/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/bin/Debug/net10.0/playwright.ps1 install chromium Absent the browser, the suite Assert.Skip's cleanly — CI without the install step still reports green. Once installed, `dotnet test` runs the scaffolding smoke in ~12s. ## Follow-up (task #242) Diagnose why `/clusters/{id}/draft/{gen}` → UNS-tab click → drag-drop flow times out under the test-owned Program.cs replica. Candidate causes: route-ordering difference, missing SignalR hub mapping timing, JS interop asset differences, culture middleware. Once the interactive circuit boots, add: - happy-path drag-drop assertion (source row → target area → Confirm → assert re-parent) - 409 conflict variant (preview → external DB mutation → Confirm → assert red-header modal)
131 lines
5.3 KiB
C#
131 lines
5.3 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests;
|
|
|
|
/// <summary>
|
|
/// Stands up the Admin Blazor Server host on a free TCP port with the live SQL Server
|
|
/// context swapped for an EF Core InMemory DbContext + the LDAP cookie auth swapped for
|
|
/// <see cref="TestAuthHandler"/>. Playwright connects to <see cref="BaseUrl"/>.
|
|
/// InMemory is sufficient because UnsService's drag-drop path exercises EF operations,
|
|
/// not raw SQL.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// We deliberately build a <see cref="WebApplication"/> directly rather than going through
|
|
/// <c>WebApplicationFactory<Program></c> — the factory's TestServer transport doesn't
|
|
/// coexist cleanly with Kestrel-on-a-real-port, and Playwright needs a real loopback HTTP
|
|
/// endpoint to hit. This mirrors the Program.cs entry-points for everything else.
|
|
/// </remarks>
|
|
public sealed class AdminWebAppFactory : IAsyncDisposable
|
|
{
|
|
private WebApplication? _app;
|
|
|
|
public string BaseUrl { get; private set; } = "";
|
|
public long SeededGenerationId { get; private set; }
|
|
public string SeededClusterId { get; } = "e2e-cluster";
|
|
|
|
public async Task StartAsync()
|
|
{
|
|
var port = GetFreeTcpPort();
|
|
BaseUrl = $"http://127.0.0.1:{port}";
|
|
|
|
var builder = WebApplication.CreateBuilder(Array.Empty<string>());
|
|
builder.WebHost.UseUrls(BaseUrl);
|
|
|
|
// --- Mirror the Admin composition in Program.cs, but with the InMemory DB + test
|
|
// auth swaps instead of SQL Server + LDAP cookie auth.
|
|
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
|
builder.Services.AddHttpContextAccessor();
|
|
builder.Services.AddSignalR();
|
|
builder.Services.AddAntiforgery();
|
|
|
|
builder.Services.AddAuthentication(TestAuthHandler.SchemeName)
|
|
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });
|
|
builder.Services.AddAuthorizationBuilder()
|
|
.AddPolicy("CanEdit", p => p.RequireRole(Admin.Services.AdminRoles.ConfigEditor, Admin.Services.AdminRoles.FleetAdmin))
|
|
.AddPolicy("CanPublish", p => p.RequireRole(Admin.Services.AdminRoles.FleetAdmin));
|
|
builder.Services.AddCascadingAuthenticationState();
|
|
|
|
builder.Services.AddDbContext<OtOpcUaConfigDbContext>(opt =>
|
|
opt.UseInMemoryDatabase($"e2e-{Guid.NewGuid():N}"));
|
|
|
|
builder.Services.AddScoped<Admin.Services.ClusterService>();
|
|
builder.Services.AddScoped<Admin.Services.GenerationService>();
|
|
builder.Services.AddScoped<Admin.Services.UnsService>();
|
|
builder.Services.AddScoped<Admin.Services.EquipmentService>();
|
|
builder.Services.AddScoped<Admin.Services.NamespaceService>();
|
|
builder.Services.AddScoped<Admin.Services.DriverInstanceService>();
|
|
builder.Services.AddScoped<Admin.Services.DraftValidationService>();
|
|
|
|
_app = builder.Build();
|
|
_app.UseStaticFiles();
|
|
_app.UseRouting();
|
|
_app.UseAuthentication();
|
|
_app.UseAuthorization();
|
|
_app.UseAntiforgery();
|
|
_app.MapRazorComponents<Admin.Components.App>().AddInteractiveServerRenderMode();
|
|
|
|
// Seed the draft BEFORE starting the host so Playwright sees a ready page on first nav.
|
|
using (var scope = _app.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<OtOpcUaConfigDbContext>();
|
|
SeededGenerationId = Seed(db, SeededClusterId);
|
|
}
|
|
|
|
await _app.StartAsync();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_app is not null)
|
|
{
|
|
await _app.StopAsync();
|
|
await _app.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
private static long Seed(OtOpcUaConfigDbContext db, string clusterId)
|
|
{
|
|
var cluster = new ServerCluster
|
|
{
|
|
ClusterId = clusterId, Name = "e2e", Enterprise = "zb", Site = "lab",
|
|
RedundancyMode = RedundancyMode.None, NodeCount = 1, CreatedBy = "e2e",
|
|
};
|
|
var gen = new ConfigGeneration
|
|
{
|
|
ClusterId = clusterId, Status = GenerationStatus.Draft, CreatedBy = "e2e",
|
|
};
|
|
|
|
db.ServerClusters.Add(cluster);
|
|
db.ConfigGenerations.Add(gen);
|
|
db.SaveChanges();
|
|
|
|
db.UnsAreas.AddRange(
|
|
new UnsArea { UnsAreaId = "area-a", ClusterId = clusterId, Name = "warsaw", GenerationId = gen.GenerationId },
|
|
new UnsArea { UnsAreaId = "area-b", ClusterId = clusterId, Name = "berlin", GenerationId = gen.GenerationId });
|
|
db.UnsLines.Add(new UnsLine
|
|
{
|
|
UnsLineId = "line-a1", UnsAreaId = "area-a", Name = "oven-line", GenerationId = gen.GenerationId,
|
|
});
|
|
db.SaveChanges();
|
|
return gen.GenerationId;
|
|
}
|
|
|
|
private static int GetFreeTcpPort()
|
|
{
|
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
listener.Stop();
|
|
return port;
|
|
}
|
|
}
|