test(control): add integration test verifying web console embedded resource delivery
This commit is contained in:
parent
989d66e998
commit
df6636bc60
1 changed files with 122 additions and 0 deletions
|
|
@ -128,8 +128,130 @@ public sealed class WebConsoleTests : IAsyncDisposable
|
||||||
Assert.Contains("отпечаток", html, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("отпечаток", html, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EmbeddedAssetsAreServedWhenDiskWebRootIsMissing()
|
||||||
|
{
|
||||||
|
await using var embeddedFactory = new WebConsoleEmbeddedOnlyTestFactory();
|
||||||
|
using var client = embeddedFactory.CreateClient();
|
||||||
|
client.DefaultRequestHeaders.Add("X-Test-Identity", "operator-admin");
|
||||||
|
client.DefaultRequestHeaders.Add("X-Test-Role", "Operator");
|
||||||
|
|
||||||
|
// 1. Root /
|
||||||
|
var rootResponse = await client.GetAsync("/", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, rootResponse.StatusCode);
|
||||||
|
Assert.Contains("text/html", rootResponse.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var rootHtml = await rootResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotEmpty(rootHtml);
|
||||||
|
Assert.Contains("id=\"nodes-table\"", rootHtml, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("id=\"add-node-btn\"", rootHtml, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// 2. /index.html
|
||||||
|
var indexResponse = await client.GetAsync("/index.html", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode);
|
||||||
|
Assert.Contains("text/html", indexResponse.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var indexHtml = await indexResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotEmpty(indexHtml);
|
||||||
|
Assert.Contains("id=\"links-table\"", indexHtml, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// 3. /style.css
|
||||||
|
var cssResponse = await client.GetAsync("/style.css", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode);
|
||||||
|
Assert.Contains("text/css", cssResponse.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var cssContent = await cssResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotEmpty(cssContent);
|
||||||
|
Assert.Contains(".app-container", cssContent, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// 4. /app.js
|
||||||
|
var jsResponse = await client.GetAsync("/app.js", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode);
|
||||||
|
Assert.Contains("javascript", jsResponse.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var jsContent = await jsResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotEmpty(jsContent);
|
||||||
|
Assert.Contains("loadDashboardData", jsContent, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// 5. /console alias
|
||||||
|
var consoleResponse = await client.GetAsync("/console", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, consoleResponse.StatusCode);
|
||||||
|
Assert.Contains("text/html", consoleResponse.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync() => await _factory.DisposeAsync();
|
public async ValueTask DisposeAsync() => await _factory.DisposeAsync();
|
||||||
|
|
||||||
|
private sealed class WebConsoleEmbeddedOnlyTestFactory : WebApplicationFactory<controlapp::Program>
|
||||||
|
{
|
||||||
|
private readonly string _directory = Path.Combine(
|
||||||
|
Path.GetTempPath(), $"smm-web-console-embedded-{Guid.NewGuid():N}");
|
||||||
|
|
||||||
|
public string DatabasePath => Path.Combine(_directory, "control.db");
|
||||||
|
|
||||||
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_directory);
|
||||||
|
var emptyWebRoot = Path.Combine(_directory, "empty-webroot-no-files");
|
||||||
|
Directory.CreateDirectory(emptyWebRoot);
|
||||||
|
builder.UseWebRoot(emptyWebRoot);
|
||||||
|
|
||||||
|
var authorityPath = Path.Combine(_directory, "control-ca.pfx");
|
||||||
|
if (!File.Exists(authorityPath))
|
||||||
|
{
|
||||||
|
using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||||
|
var request = new CertificateRequest("CN=SMM Web Console Embedded CA", key, HashAlgorithmName.SHA256);
|
||||||
|
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
|
||||||
|
request.CertificateExtensions.Add(new X509KeyUsageExtension(
|
||||||
|
X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
|
||||||
|
using var certificate = request.CreateSelfSigned(
|
||||||
|
DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddDays(1));
|
||||||
|
File.WriteAllBytes(authorityPath, certificate.Export(X509ContentType.Pfx));
|
||||||
|
}
|
||||||
|
|
||||||
|
var publicUrlPath = Path.Combine(_directory, "control-public-url");
|
||||||
|
File.WriteAllText(publicUrlPath, "https://hub.example.com:7443\n");
|
||||||
|
|
||||||
|
var meshEnvPath = Path.Combine(_directory, "mesh.env");
|
||||||
|
File.WriteAllText(meshEnvPath, "HUB_ENDPOINT=hub.example.com:51820\nHUB_PUBLIC_KEY=mQZ/Y4yQpQhX6j0rL8vU2w==\nMESH_NETWORK=10.77.0.0/24\n");
|
||||||
|
|
||||||
|
var meshNodesPath = Path.Combine(_directory, "mesh", "nodes.tsv");
|
||||||
|
|
||||||
|
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||||
|
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["Control:DatabasePath"] = DatabasePath,
|
||||||
|
["Control:CertificateAuthorityPath"] = authorityPath,
|
||||||
|
["Control:BackupDirectory"] = Path.Combine(_directory, "backups"),
|
||||||
|
["Control:PublicUrlPath"] = publicUrlPath,
|
||||||
|
["Control:MeshEnvironmentPath"] = meshEnvPath,
|
||||||
|
["Control:MeshNodesPath"] = meshNodesPath
|
||||||
|
}));
|
||||||
|
|
||||||
|
builder.ConfigureTestServices(services =>
|
||||||
|
{
|
||||||
|
services.RemoveAll<IHostedService>();
|
||||||
|
services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = "Test";
|
||||||
|
options.DefaultChallengeScheme = "Test";
|
||||||
|
})
|
||||||
|
.AddScheme<AuthenticationSchemeOptions, TestAuthenticationHandler>("Test", _ => { });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await base.DisposeAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_directory))
|
||||||
|
{
|
||||||
|
Directory.Delete(_directory, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignored in test cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class WebConsoleTestFactory : WebApplicationFactory<controlapp::Program>
|
private sealed class WebConsoleTestFactory : WebApplicationFactory<controlapp::Program>
|
||||||
{
|
{
|
||||||
private readonly string _directory = Path.Combine(
|
private readonly string _directory = Path.Combine(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue