diff --git a/tests/ScadaLink.CentralUI.PlaywrightTests/PlaywrightFixture.cs b/tests/ScadaLink.CentralUI.PlaywrightTests/PlaywrightFixture.cs
index 0a8ba97..995462e 100644
--- a/tests/ScadaLink.CentralUI.PlaywrightTests/PlaywrightFixture.cs
+++ b/tests/ScadaLink.CentralUI.PlaywrightTests/PlaywrightFixture.cs
@@ -49,11 +49,17 @@ public class PlaywrightFixture : IAsyncLifetime
}
///
- /// Create a new page and log in with the test user.
+ /// Create a new page and log in with the default multi-role test user.
+ ///
+ public Task NewAuthenticatedPageAsync() =>
+ NewAuthenticatedPageAsync(TestUsername, TestPassword);
+
+ ///
+ /// Create a new page and log in with specific credentials.
/// Uses JavaScript fetch() to POST to /auth/login from within the browser,
/// which sets the auth cookie in the browser context. Then navigates to the dashboard.
///
- public async Task NewAuthenticatedPageAsync()
+ public async Task NewAuthenticatedPageAsync(string username, string password)
{
var page = await NewPageAsync();
@@ -63,24 +69,21 @@ public class PlaywrightFixture : IAsyncLifetime
// POST to /auth/login via fetch() inside the browser.
// This sets the auth cookie in the browser context automatically.
- // Use redirect: 'follow' so the browser follows the 302 and the cookie is stored.
var finalUrl = await page.EvaluateAsync(@"
- async () => {
+ async ([u, p]) => {
const resp = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: 'username=' + encodeURIComponent('" + TestUsername + @"')
- + '&password=' + encodeURIComponent('" + TestPassword + @"'),
+ body: 'username=' + encodeURIComponent(u) + '&password=' + encodeURIComponent(p),
redirect: 'follow'
});
return resp.url;
}
- ");
+ ", new object[] { username, password });
- // The fetch followed the redirect. If it ended on /login, auth failed.
if (finalUrl.Contains("/login"))
{
- throw new InvalidOperationException($"Login failed — redirected back to login: {finalUrl}");
+ throw new InvalidOperationException($"Login failed for '{username}' — redirected back to login: {finalUrl}");
}
// Navigate to the dashboard — cookie authenticates us
diff --git a/tests/ScadaLink.CentralUI.PlaywrightTests/RoleNavigationTests.cs b/tests/ScadaLink.CentralUI.PlaywrightTests/RoleNavigationTests.cs
new file mode 100644
index 0000000..0e85017
--- /dev/null
+++ b/tests/ScadaLink.CentralUI.PlaywrightTests/RoleNavigationTests.cs
@@ -0,0 +1,226 @@
+using Microsoft.Playwright;
+
+namespace ScadaLink.CentralUI.PlaywrightTests;
+
+///
+/// Verifies that navigation sections and links are shown/hidden based on the user's role.
+///
+/// LDAP test users (all passwords: "password"):
+/// admin → Admin only
+/// designer → Design only
+/// deployer → Deployment only
+/// multi-role → Admin + Design + Deployment
+///
+/// Nav structure (from NavMenu.razor):
+/// All authenticated: Dashboard, Monitoring (Health Dashboard, Event Logs, Parked Messages)
+/// Admin: LDAP Mappings, Sites, Data Connections, API Keys, Audit Log
+/// Design: Templates, Shared Scripts, External Systems, Areas
+/// Deployment: Instances, Deployments, Debug View
+///
+[Collection("Playwright")]
+public class RoleNavigationTests
+{
+ private readonly PlaywrightFixture _fixture;
+
+ public RoleNavigationTests(PlaywrightFixture fixture)
+ {
+ _fixture = fixture;
+ }
+
+ // ── Admin-only user ─────────────────────────────────────────────
+
+ [Fact]
+ public async Task AdminUser_SeesAdminSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("admin", "password");
+
+ await AssertNavLinkVisible(page, "Sites");
+ await AssertNavLinkVisible(page, "Data Connections");
+ await AssertNavLinkVisible(page, "API Keys");
+ await AssertNavLinkVisible(page, "LDAP Mappings");
+ await AssertNavLinkVisible(page, "Audit Log");
+ }
+
+ [Fact]
+ public async Task AdminUser_DoesNotSeeDesignSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("admin", "password");
+
+ await AssertNavLinkHidden(page, "Templates");
+ await AssertNavLinkHidden(page, "Shared Scripts");
+ await AssertNavLinkHidden(page, "External Systems");
+ }
+
+ [Fact]
+ public async Task AdminUser_DoesNotSeeDeploymentSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("admin", "password");
+
+ await AssertNavLinkHidden(page, "Instances");
+ await AssertNavLinkHidden(page, "Deployments");
+ await AssertNavLinkHidden(page, "Debug View");
+ }
+
+ [Fact]
+ public async Task AdminUser_SeesMonitoringSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("admin", "password");
+
+ await AssertNavLinkVisible(page, "Health Dashboard");
+ await AssertNavLinkVisible(page, "Event Logs");
+ await AssertNavLinkVisible(page, "Parked Messages");
+ }
+
+ // ── Design-only user ────────────────────────────────────────────
+
+ [Fact]
+ public async Task DesignUser_SeesDesignSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("designer", "password");
+
+ await AssertNavLinkVisible(page, "Templates");
+ await AssertNavLinkVisible(page, "Shared Scripts");
+ await AssertNavLinkVisible(page, "External Systems");
+ await AssertNavLinkVisible(page, "Areas");
+ }
+
+ [Fact]
+ public async Task DesignUser_DoesNotSeeAdminSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("designer", "password");
+
+ await AssertNavLinkHidden(page, "Sites");
+ await AssertNavLinkHidden(page, "Data Connections");
+ await AssertNavLinkHidden(page, "API Keys");
+ await AssertNavLinkHidden(page, "LDAP Mappings");
+ await AssertNavLinkHidden(page, "Audit Log");
+ }
+
+ [Fact]
+ public async Task DesignUser_DoesNotSeeDeploymentSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("designer", "password");
+
+ await AssertNavLinkHidden(page, "Instances");
+ await AssertNavLinkHidden(page, "Deployments");
+ await AssertNavLinkHidden(page, "Debug View");
+ }
+
+ [Fact]
+ public async Task DesignUser_SeesMonitoringButNotAuditLog()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("designer", "password");
+
+ await AssertNavLinkVisible(page, "Health Dashboard");
+ await AssertNavLinkVisible(page, "Event Logs");
+ await AssertNavLinkVisible(page, "Parked Messages");
+ await AssertNavLinkHidden(page, "Audit Log");
+ }
+
+ // ── Deployment-only user ────────────────────────────────────────
+
+ [Fact]
+ public async Task DeploymentUser_SeesDeploymentSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("deployer", "password");
+
+ await AssertNavLinkVisible(page, "Instances");
+ await AssertNavLinkVisible(page, "Deployments");
+ await AssertNavLinkVisible(page, "Debug View");
+ }
+
+ [Fact]
+ public async Task DeploymentUser_DoesNotSeeAdminSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("deployer", "password");
+
+ await AssertNavLinkHidden(page, "Sites");
+ await AssertNavLinkHidden(page, "Data Connections");
+ await AssertNavLinkHidden(page, "API Keys");
+ await AssertNavLinkHidden(page, "LDAP Mappings");
+ await AssertNavLinkHidden(page, "Audit Log");
+ }
+
+ [Fact]
+ public async Task DeploymentUser_DoesNotSeeDesignSection()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("deployer", "password");
+
+ await AssertNavLinkHidden(page, "Templates");
+ await AssertNavLinkHidden(page, "Shared Scripts");
+ await AssertNavLinkHidden(page, "External Systems");
+ }
+
+ [Fact]
+ public async Task DeploymentUser_SeesMonitoringButNotAuditLog()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("deployer", "password");
+
+ await AssertNavLinkVisible(page, "Health Dashboard");
+ await AssertNavLinkVisible(page, "Event Logs");
+ await AssertNavLinkVisible(page, "Parked Messages");
+ await AssertNavLinkHidden(page, "Audit Log");
+ }
+
+ // ── Multi-role user (Admin + Design + Deployment) ───────────────
+
+ [Fact]
+ public async Task MultiRoleUser_SeesAllSections()
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password");
+
+ // Admin
+ await AssertNavLinkVisible(page, "Sites");
+ await AssertNavLinkVisible(page, "Data Connections");
+ await AssertNavLinkVisible(page, "API Keys");
+ await AssertNavLinkVisible(page, "LDAP Mappings");
+ await AssertNavLinkVisible(page, "Audit Log");
+
+ // Design
+ await AssertNavLinkVisible(page, "Templates");
+ await AssertNavLinkVisible(page, "Shared Scripts");
+ await AssertNavLinkVisible(page, "External Systems");
+ await AssertNavLinkVisible(page, "Areas");
+
+ // Deployment
+ await AssertNavLinkVisible(page, "Instances");
+ await AssertNavLinkVisible(page, "Deployments");
+ await AssertNavLinkVisible(page, "Debug View");
+
+ // Monitoring (all authenticated)
+ await AssertNavLinkVisible(page, "Health Dashboard");
+ await AssertNavLinkVisible(page, "Event Logs");
+ await AssertNavLinkVisible(page, "Parked Messages");
+ }
+
+ // ── All users see Dashboard ─────────────────────────────────────
+
+ [Theory]
+ [InlineData("admin")]
+ [InlineData("designer")]
+ [InlineData("deployer")]
+ [InlineData("multi-role")]
+ public async Task AllUsers_SeeDashboardLink(string username)
+ {
+ var page = await _fixture.NewAuthenticatedPageAsync(username, "password");
+
+ var dashboardLink = page.GetByRole(AriaRole.Link, new() { Name = "Dashboard", Exact = true });
+ await Assertions.Expect(dashboardLink).ToBeVisibleAsync();
+ }
+
+ // ── Helpers ─────────────────────────────────────────────────────
+
+ private static async Task AssertNavLinkVisible(IPage page, string linkText)
+ {
+ var locator = page.Locator($"nav a:has-text('{linkText}')");
+ var count = await locator.CountAsync();
+ Assert.True(count > 0, $"Expected nav link '{linkText}' to be visible, but it was not found");
+ }
+
+ private static async Task AssertNavLinkHidden(IPage page, string linkText)
+ {
+ var locator = page.Locator($"nav a:has-text('{linkText}')");
+ var count = await locator.CountAsync();
+ Assert.True(count == 0, $"Expected nav link '{linkText}' to be hidden, but it was found");
+ }
+}