Files
scadalink-design/tests/ScadaLink.IntegrationTests/AuthFlowTests.cs
Joseph Doherty d38356efdb Phase 1 WP-11–22: Host infrastructure, Blazor Server UI, and integration tests
Host infrastructure (WP-11–17):
- StartupValidator with 19 validation rules
- /health/ready endpoint with DB + Akka health checks
- Akka.NET bootstrap via AkkaHostedService (HOCON config, cluster, remoting, SBR)
- Serilog with SiteId/NodeHostname/NodeRole enrichment
- DeadLetterMonitorActor with count tracking
- CoordinatedShutdown wiring (no Environment.Exit)
- Windows Service support (UseWindowsService)

Central UI (WP-18–21):
- Blazor Server shell with Bootstrap 5, role-aware NavMenu
- Login/logout flow (LDAP auth → JWT → HTTP-only cookie)
- CookieAuthenticationStateProvider with idle timeout
- LDAP group mapping CRUD page (Admin role)
- Route guards with Authorize attributes per role
- SignalR reconnection overlay for failover

Integration tests (WP-22):
- Startup validation, auth flow, audit transactions, readiness gating
186 tests pass (1 skipped: LDAP integration), zero warnings.
2026-03-16 19:50:59 -04:00

133 lines
4.8 KiB
C#

using System.Net;
using Microsoft.Extensions.DependencyInjection;
using ScadaLink.CentralUI.Auth;
using ScadaLink.Security;
namespace ScadaLink.IntegrationTests;
/// <summary>
/// WP-22: Auth flow integration tests.
/// Tests that require a running LDAP server are marked with Integration trait.
/// </summary>
public class AuthFlowTests : IClassFixture<ScadaLinkWebApplicationFactory>
{
private readonly ScadaLinkWebApplicationFactory _factory;
public AuthFlowTests(ScadaLinkWebApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task LoginEndpoint_WithEmptyCredentials_RedirectsToLoginWithError()
{
var client = _factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("username", ""),
new KeyValuePair<string, string>("password", "")
});
var response = await client.PostAsync("/auth/login", content);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
var location = response.Headers.Location?.ToString() ?? "";
Assert.Contains("/login", location);
Assert.Contains("error", location, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task LogoutEndpoint_ClearsCookieAndRedirects()
{
var client = _factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var response = await client.PostAsync("/auth/logout", null);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
var location = response.Headers.Location?.ToString() ?? "";
Assert.Contains("/login", location);
}
[Fact]
public void JwtTokenService_GenerateAndValidate_RoundTrips()
{
using var scope = _factory.Services.CreateScope();
var jwtService = scope.ServiceProvider.GetRequiredService<JwtTokenService>();
var token = jwtService.GenerateToken(
displayName: "Test User",
username: "testuser",
roles: new[] { "Admin", "Design" },
permittedSiteIds: null);
Assert.NotNull(token);
var principal = jwtService.ValidateToken(token);
Assert.NotNull(principal);
var displayName = principal!.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value;
var username = principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value;
var roles = principal.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value).ToList();
Assert.Equal("Test User", displayName);
Assert.Equal("testuser", username);
Assert.Contains("Admin", roles);
Assert.Contains("Design", roles);
}
[Fact]
public void JwtTokenService_WithSiteScopes_IncludesSiteIdClaims()
{
using var scope = _factory.Services.CreateScope();
var jwtService = scope.ServiceProvider.GetRequiredService<JwtTokenService>();
var token = jwtService.GenerateToken(
displayName: "Deployer",
username: "deployer1",
roles: new[] { "Deployment" },
permittedSiteIds: new[] { "1", "3" });
var principal = jwtService.ValidateToken(token);
Assert.NotNull(principal);
var siteIds = principal!.FindAll(JwtTokenService.SiteIdClaimType).Select(c => c.Value).ToList();
Assert.Contains("1", siteIds);
Assert.Contains("3", siteIds);
}
[Trait("Category", "Integration")]
[Fact(Skip = "Requires running GLAuth LDAP server (Docker). Run with: docker compose -f infra/docker-compose.yml up -d glauth")]
public async Task LoginEndpoint_WithValidLdapCredentials_SetsCookieAndRedirects()
{
// This test requires the GLAuth test LDAP server running on localhost:3893
var client = _factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("username", "admin"),
new KeyValuePair<string, string>("password", "admin")
});
var response = await client.PostAsync("/auth/login", content);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
var location = response.Headers.Location?.ToString() ?? "";
Assert.Equal("/", location);
// Verify auth cookie was set
var setCookieHeader = response.Headers.GetValues("Set-Cookie").FirstOrDefault();
Assert.NotNull(setCookieHeader);
Assert.Contains(CookieAuthenticationStateProvider.AuthCookieName, setCookieHeader);
}
}