Merge commit '46279f740b584ac64398033804d759cc4c64fa97'
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# Test Evidence: persistent Admin SMTP settings navigation
|
||||
|
||||
- **Test type:** Web
|
||||
- **Requirement IDs:** `ACC-007`, `INT-001`, `AUTH-002`, `UI-003`, `UI-008`, `UI-009`, `UI-010`
|
||||
- **Scenario IDs:** `AC-ACC-003`, `AC-UI-002`, `AC-UI-003`
|
||||
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.DashboardControllerWebTest`, `com.lab.labtimesheet.feature.reporting.controller.RoleDashboardWebIntegrationTest#mentorAndInternDashboardsRenderRealScopedProjectTaskAndAttendanceData`
|
||||
- **Implementation commit:** pending
|
||||
|
||||
## Protected behavior
|
||||
|
||||
An Admin always receives an SMTP settings destination in the shared sidebar, whether SMTP is restricted or active. The restricted-installation warning remains conditional. Mentor and Intern sidebars never expose the Admin-only destination, and the Admin link uses the local settings sprite plus the established collapsed-sidebar tooltip.
|
||||
|
||||
## Test method
|
||||
|
||||
The MVC slice renders the real dashboard controller, Spring Security Thymeleaf dialect, and shared layout with only the SMTP state and dashboard query services mocked at their public boundaries. It checks both Admin SMTP states and the active-SMTP Mentor/Intern views. The PostgreSQL 18.4 integration test activates SMTP through the real service, extracts rendered navigation links, requires the Admin SMTP route only for Admin, and follows every discovered link through the real controller/security stack.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
With SMTP restricted, an Admin dashboard contains the existing warning and a sidebar link to `/admin/smtp` identified by `data-tooltip="SMTP settings"`. After SMTP activation, the warning is absent but that same sidebar link remains. Mentor and Intern dashboards omit the SMTP-settings tooltip and route. The activated Admin link resolves successfully when followed.
|
||||
|
||||
## RED
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/opt/node@24/bin:$PATH ./mvnw -DargLine=-javaagent:/Users/sechmachine/.m2/repository/net/bytebuddy/byte-buddy-agent/1.18.10/byte-buddy-agent-1.18.10.jar -Dtest=DashboardControllerWebTest test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Tests run: 6, Failures: 2, Errors: 0, Skipped: 0
|
||||
DashboardControllerWebTest.adminRendersAdminDashboardForAuthenticatedIdentity: expected data-tooltip="SMTP settings" but it was absent.
|
||||
DashboardControllerWebTest.activeSmtpKeepsAdminDashboardFreeOfTheRestrictedInstallationWarning: expected href="/admin/smtp" data-tooltip="SMTP settings" but it was absent.
|
||||
BUILD FAILURE
|
||||
```
|
||||
|
||||
The Mentor and Intern active-SMTP assertions passed in this RED run, so the failures establish the missing Admin navigation rather than an incorrect role fixture.
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/opt/node@24/bin:$PATH DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock ./mvnw -DargLine=-javaagent:/Users/sechmachine/.m2/repository/net/bytebuddy/byte-buddy-agent/1.18.10/byte-buddy-agent-1.18.10.jar -Dtest=RoleDashboardWebIntegrationTest test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
PostgreSQL: 18.4 Testcontainer
|
||||
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
|
||||
Expected Admin visible navigation paths to contain /admin/smtp, but rendered paths were /dashboard, /admin/accounts/new, /attendance/calendar.
|
||||
BUILD FAILURE
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/opt/node@24/bin:$PATH ./mvnw -DargLine=-javaagent:/Users/sechmachine/.m2/repository/net/bytebuddy/byte-buddy-agent/1.18.10/byte-buddy-agent-1.18.10.jar -Dtest=DashboardControllerWebTest test
|
||||
|
||||
JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/opt/node@24/bin:$PATH DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock ./mvnw -DargLine=-javaagent:/Users/sechmachine/.m2/repository/net/bytebuddy/byte-buddy-agent/1.18.10/byte-buddy-agent-1.18.10.jar -Dtest=RoleDashboardWebIntegrationTest test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
DashboardControllerWebTest: Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
|
||||
RoleDashboardWebIntegrationTest: PostgreSQL 18.4 Testcontainer; Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
```
|
||||
|
||||
## Affected suite
|
||||
|
||||
**Command and result**
|
||||
|
||||
```text
|
||||
PATH=/opt/homebrew/opt/node@24/bin:$PATH node --version && PATH=/opt/homebrew/opt/node@24/bin:$PATH npm --version && PATH=/opt/homebrew/opt/node@24/bin:$PATH npm ci && PATH=/opt/homebrew/opt/node@24/bin:$PATH npm run build
|
||||
Node v24.19.0; npm 11.17.0; Tailwind CSS v4.3.3
|
||||
BUILD SUCCESS
|
||||
|
||||
JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/opt/node@24/bin:$PATH DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock ./mvnw -DargLine=-javaagent:/Users/sechmachine/.m2/repository/net/bytebuddy/byte-buddy-agent/1.18.10/byte-buddy-agent-1.18.10.jar -Dtest=SecurityResponseIntegrationTest,BootstrapOnboardingWebIntegrationTest,AccountWebIntegrationTest,SmtpOnboardingWebIntegrationTest,RoleDashboardWebIntegrationTest,UiContractWebTest,AccountTemplateIntegrationTest,AttendanceTemplateIntegrationTest,DashboardControllerWebTest,DashboardTemplateWebTest,ProjectTaskFormAccessibilityWebTest,SharedErrorTemplateWebTest,ProjectControllerTest,TaskControllerTest,AttendanceControllerTest test
|
||||
PostgreSQL 18.4 Testcontainers; Tests run: 81, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
MockMvc proves rendered role/state visibility, while the PostgreSQL integration test proves the real Admin route follow. They do not render the collapsed rail or inspect pixels, browser focus placement, or tooltip positioning; the existing CSS and local settings sprite are reused unchanged. Server-side direct-URL authorization remains the existing `/admin/**` Admin-only security rule and is not broadened by this layout-only change.
|
||||
@@ -23,6 +23,7 @@
|
||||
<li><a class="nav-link" th:href="@{/dashboard}" data-tooltip="Overview" th:attr="aria-current=${activeNav == 'dashboard'} ? 'page' : null">
|
||||
<svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#layout-dashboard}"></use></svg><span class="sidebar-label">Overview</span></a></li>
|
||||
<li sec:authorize="hasRole('ADMIN')"><a class="nav-link" th:href="@{/admin/accounts/new}" data-tooltip="Accounts" th:attr="aria-current=${activeNav == 'accounts'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#users}"></use></svg><span class="sidebar-label">Accounts</span></a></li>
|
||||
<li sec:authorize="hasRole('ADMIN')"><a class="nav-link" th:href="@{/admin/smtp}" data-tooltip="SMTP settings" th:attr="aria-current=${activeNav == 'smtp'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#settings}"></use></svg><span class="sidebar-label">SMTP settings</span></a></li>
|
||||
<li sec:authorize="hasRole('ADMIN')"><a class="nav-link" th:href="@{/attendance/calendar}" data-tooltip="Global calendar" th:attr="aria-current=${activeNav == 'calendar'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#calendar-days}"></use></svg><span class="sidebar-label">Global calendar</span></a></li>
|
||||
<li sec:authorize="hasRole('MENTOR')"><a class="nav-link" th:href="@{/projects}" data-tooltip="Owned Projects" th:attr="aria-current=${activeNav == 'projects'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#folder-kanban}"></use></svg><span class="sidebar-label">Owned Projects</span></a></li>
|
||||
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/attendance}" data-tooltip="My attendance" th:attr="aria-current=${activeNav == 'attendance'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#clock}"></use></svg><span class="sidebar-label">My attendance</span></a></li>
|
||||
|
||||
+14
-4
@@ -44,7 +44,9 @@ class DashboardControllerWebTest {
|
||||
.andExpect(model().attribute("dashboard", dashboard))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||
"This installation remains restricted until tested SMTP is active.")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("href=\"/admin/smtp\"")));
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("href=\"/admin/smtp\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||
"data-tooltip=\"SMTP settings\"")));
|
||||
|
||||
verify(dashboards).admin("admin@example.test");
|
||||
}
|
||||
@@ -58,20 +60,25 @@ class DashboardControllerWebTest {
|
||||
mvc.perform(get("/dashboard").with(user("admin@example.test").roles("ADMIN")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.not(org.hamcrest.Matchers.containsString(
|
||||
"This installation remains restricted until tested SMTP is active."))));
|
||||
"This installation remains restricted until tested SMTP is active."))))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||
"href=\"/admin/smtp\" data-tooltip=\"SMTP settings\"")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mentorRendersMentorDashboardForAuthenticatedIdentity() throws Exception {
|
||||
var dashboard = new DashboardView.Mentor("Mentor", 2, 4, 1);
|
||||
given(dashboards.mentor("mentor@example.test")).willReturn(dashboard);
|
||||
given(smtpConfiguration.hasActiveConfiguration()).willReturn(true);
|
||||
|
||||
mvc.perform(get("/dashboard").with(user("mentor@example.test").roles("MENTOR")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("dashboard/mentor"))
|
||||
.andExpect(model().attribute("dashboard", dashboard))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.not(org.hamcrest.Matchers.containsString(
|
||||
"This installation remains restricted until tested SMTP is active."))));
|
||||
"This installation remains restricted until tested SMTP is active."))))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.not(org.hamcrest.Matchers.containsString(
|
||||
"data-tooltip=\"SMTP settings\""))));
|
||||
|
||||
verify(dashboards).mentor("mentor@example.test");
|
||||
}
|
||||
@@ -81,11 +88,14 @@ class DashboardControllerWebTest {
|
||||
var dashboard = new DashboardView.Intern(
|
||||
"Intern", DashboardView.AttendanceState.NOT_CHECKED_IN, 1, 0, List.of());
|
||||
given(dashboards.intern("intern@example.test")).willReturn(dashboard);
|
||||
given(smtpConfiguration.hasActiveConfiguration()).willReturn(true);
|
||||
|
||||
mvc.perform(get("/dashboard").with(user("intern@example.test").roles("INTERN")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("dashboard/intern"))
|
||||
.andExpect(model().attribute("dashboard", dashboard));
|
||||
.andExpect(model().attribute("dashboard", dashboard))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.not(org.hamcrest.Matchers.containsString(
|
||||
"data-tooltip=\"SMTP settings\""))));
|
||||
|
||||
verify(dashboards).intern("intern@example.test");
|
||||
}
|
||||
|
||||
+9
-4
@@ -136,12 +136,12 @@ class RoleDashboardWebIntegrationTest {
|
||||
.andExpect(content().string(containsString("BLOCKED")))
|
||||
.andExpect(content().string(containsString("20/08/2026")));
|
||||
|
||||
followEveryVisibleNavigationLink("admin@example.test", "ADMIN");
|
||||
followEveryVisibleNavigationLink("mentor@example.test", "MENTOR");
|
||||
followEveryVisibleNavigationLink("intern@example.test", "INTERN");
|
||||
followEveryVisibleNavigationLink("admin@example.test", "ADMIN", true);
|
||||
followEveryVisibleNavigationLink("mentor@example.test", "MENTOR", false);
|
||||
followEveryVisibleNavigationLink("intern@example.test", "INTERN", false);
|
||||
}
|
||||
|
||||
private void followEveryVisibleNavigationLink(String email, String role) throws Exception {
|
||||
private void followEveryVisibleNavigationLink(String email, String role, boolean expectsSmtpSettings) throws Exception {
|
||||
String dashboard = mvc.perform(get("/dashboard").with(user(email).roles(role)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
@@ -153,6 +153,11 @@ class RoleDashboardWebIntegrationTest {
|
||||
paths.add(matcher.group(1));
|
||||
}
|
||||
assertThat(paths).isNotEmpty();
|
||||
if (expectsSmtpSettings) {
|
||||
assertThat(paths).contains("/admin/smtp");
|
||||
} else {
|
||||
assertThat(paths).doesNotContain("/admin/smtp");
|
||||
}
|
||||
for (String path : paths) {
|
||||
mvc.perform(get(path).with(user(email).roles(role)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
Reference in New Issue
Block a user