fix(ui): persist SMTP restriction in shared shell
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
# Test Evidence: persistent SMTP warning and accessible onboarding shell
|
||||||
|
|
||||||
|
- **Test type:** Web
|
||||||
|
- **Requirement IDs:** `ACC-005`, `ACC-006`, `ACC-007`, `INT-007`, `UI-004`, `UI-007`, `UI-010`, `I1-UI-01`, `I1-UI-02`, `I1-UI-04`
|
||||||
|
- **Scenario IDs:** `AC-ACC-003`, `AC-UI-001`, `AC-UI-002`
|
||||||
|
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.DashboardControllerWebTest`, `com.lab.labtimesheet.feature.integration.controller.SmtpOnboardingWebIntegrationTest`, `com.lab.labtimesheet.feature.account.controller.BootstrapOnboardingWebIntegrationTest#fiveDistinctDeferralConfirmationsAreSequentialAndOnlyTheLastCanFinish`
|
||||||
|
- **Implementation commit:** `pending`
|
||||||
|
|
||||||
|
## Protected behavior
|
||||||
|
|
||||||
|
An Admin without active SMTP sees a persistent, actionable restricted-installation warning on every shared-shell page, including the dashboard reached after the fifth deferral confirmation. The warning is absent for non-Admins and after SMTP activation. SMTP configuration and deferral reuse the authenticated desktop shell and its pre-paint theme, focus, local assets, navigation, and logout behavior. Invalid SMTP fields expose a single accessible error summary plus stable field-error IDs referenced by the corresponding controls, while safe fields are retained and the submitted password is never rendered.
|
||||||
|
|
||||||
|
## Test method
|
||||||
|
|
||||||
|
The reporting MVC slice renders the production Admin and Mentor dashboard templates with real Spring Security principals and only the dashboard and SMTP services mocked at their public boundaries. PostgreSQL 18.4 integration tests bootstrap a real Admin, traverse all five server-owned deferral steps, finish onto the real dashboard, and render a representative account page. The SMTP integration test submits every supported invalid field combination through the real controller, Jakarta Validation, Thymeleaf binding, and production template. A separate invalid request proves safe-value retention and request-local password clearing.
|
||||||
|
|
||||||
|
## Hand-derived expected result
|
||||||
|
|
||||||
|
With no active SMTP, an Admin dashboard and account page contain the exact warning and an `/admin/smtp` action. A Mentor dashboard never contains that warning, and an Admin page after activation does not contain it. The first through fourth deferral steps do not expose Finish; the fifth does; Finish redirects to `/dashboard`, where the warning persists. Host, port, security mode, authentication completeness, From address, and From name each render a unique error ID and the associated invalid control references that ID through `aria-describedby`. The global summary is labeled, safe username/sender values remain, and the submitted password is absent.
|
||||||
|
|
||||||
|
## RED
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw -Dtest=DashboardControllerWebTest,SmtpOnboardingWebIntegrationTest,BootstrapOnboardingWebIntegrationTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tests run: 13, Failures: 3, Errors: 0, Skipped: 0
|
||||||
|
DashboardControllerWebTest: Admin dashboard did not contain the persistent restricted-installation warning or SMTP action.
|
||||||
|
SmtpOnboardingWebIntegrationTest: SMTP form did not load /assets/theme.js because it was still standalone.
|
||||||
|
BootstrapOnboardingWebIntegrationTest: SMTP deferral did not load /assets/theme.js because it was still standalone.
|
||||||
|
BUILD FAILURE
|
||||||
|
```
|
||||||
|
|
||||||
|
The initial XPath assertion attempt was discarded before implementation because the HTML5 doctype is not XML-parseable by MockMvc's XML XPath matcher. The corrected string-based run above is the recorded behavior RED.
|
||||||
|
|
||||||
|
A follow-up focused RED for the authentication-pair error ran one PostgreSQL-backed method and failed 1/1 because the password referenced `smtp-authentication-error` but the paired username did not. Associating both controls made the identical command pass 1/1.
|
||||||
|
|
||||||
|
## GREEN
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw -Dtest=DashboardControllerWebTest,SmtpOnboardingWebIntegrationTest,BootstrapOnboardingWebIntegrationTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
PostgreSQL 18.4 via Testcontainers
|
||||||
|
Tests run: 14, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
Total time: 30.179 s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Affected suite
|
||||||
|
|
||||||
|
**Command and result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
npm run build
|
||||||
|
./mvnw -Dtest=SecurityResponseIntegrationTest,BootstrapOnboardingWebIntegrationTest,AccountWebIntegrationTest,SmtpOnboardingWebIntegrationTest,RoleDashboardWebIntegrationTest,UiContractWebTest,AccountTemplateIntegrationTest,AttendanceTemplateIntegrationTest,DashboardControllerWebTest,DashboardTemplateWebTest,ProjectTaskFormAccessibilityWebTest,SharedErrorTemplateWebTest,ProjectControllerTest,TaskControllerTest,AttendanceControllerTest test
|
||||||
|
|
||||||
|
Node v24.19.0; npm 11.17.0; Tailwind CSS v4.3.3
|
||||||
|
Two consecutive builds produced app.css SHA-256 f0a4abbffaf66581ee7e17952743e591b8957e0cbcd19099e234d13827700e4c and icons.svg SHA-256 001f72c93967f816fdd56f3f9b34cb5e5831b8b8c572d051669c6a3aae2c3cda.
|
||||||
|
PostgreSQL 18.4 via Testcontainers
|
||||||
|
Tests run: 81, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
Total time: 50.418 s
|
||||||
|
```
|
||||||
|
|
||||||
|
## External-test boundaries
|
||||||
|
|
||||||
|
MockMvc verifies rendered security visibility, form binding, CSRF-generated forms, exact deferral ordering, safe retained values, and accessibility associations. It does not prove viewport overflow, keyboard focus rendering, collapse behavior, or visually observable theme flash; the separate real-browser evidence covers those boundaries. SMTP transport remains represented by the existing test probe and no real mail server is required.
|
||||||
@@ -152,6 +152,8 @@
|
|||||||
.badge-warning { color: var(--warning); }
|
.badge-warning { color: var(--warning); }
|
||||||
.badge-danger { color: var(--danger); }
|
.badge-danger { color: var(--danger); }
|
||||||
.alert { margin: .75rem 0; border: 1px solid var(--border); border-radius: .6rem; padding: .75rem .9rem; }
|
.alert { margin: .75rem 0; border: 1px solid var(--border); border-radius: .6rem; padding: .75rem .9rem; }
|
||||||
|
.alert-warning { border-color: color-mix(in srgb, var(--warning), transparent 55%); color: var(--warning); }
|
||||||
|
.alert-action { margin-left: .6rem; font-weight: 700; }
|
||||||
.alert-error { border-color: color-mix(in srgb, var(--danger), transparent 60%); color: var(--danger); }
|
.alert-error { border-color: color-mix(in srgb, var(--danger), transparent 60%); color: var(--danger); }
|
||||||
.empty-state { padding: 2.5rem 1rem; text-align: center; }
|
.empty-state { padding: 2.5rem 1rem; text-align: center; }
|
||||||
.empty-state p { margin: .3rem auto 0; color: var(--muted); }
|
.empty-state p { margin: .3rem auto 0; color: var(--muted); }
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -10,7 +10,6 @@
|
|||||||
<a id="primary-action" class="button" th:href="@{/dashboard}">Back to overview</a>
|
<a id="primary-action" class="button" th:href="@{/dashboard}">Back to overview</a>
|
||||||
<main>
|
<main>
|
||||||
<p class="page-description">Create a role-specific account and send its one-time activation link.</p>
|
<p class="page-description">Create a role-specific account and send its one-time activation link.</p>
|
||||||
<p class="alert alert-error" th:if="${smtpRestricted}" role="alert">This is a restricted installation until tested SMTP is active.</p>
|
|
||||||
<p class="alert" th:if="${param.created}" role="status">Account created and activation email sent.</p>
|
<p class="alert" th:if="${param.created}" role="status">Account created and activation email sent.</p>
|
||||||
<p class="alert alert-error" th:if="${param.deliveryFailed}" role="alert">Account created, but activation delivery failed.</p>
|
<p class="alert alert-error" th:if="${param.deliveryFailed}" role="alert">Account created, but activation delivery failed.</p>
|
||||||
<form class="panel form-panel form-grid" method="post" th:action="@{/admin/accounts}" th:object="${accountForm}">
|
<form class="panel form-panel form-grid" method="post" th:action="@{/admin/accounts}" th:object="${accountForm}">
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
<div class="page-heading-copy"><h1 class="page-title" th:text="${pageTitle}">Page</h1></div>
|
<div class="page-heading-copy"><h1 class="page-title" th:text="${pageTitle}">Page</h1></div>
|
||||||
<div class="primary-action" th:if="${primaryAction != null}" th:replace="${primaryAction}"></div>
|
<div class="primary-action" th:if="${primaryAction != null}" th:replace="${primaryAction}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="alert alert-warning" role="alert" sec:authorize="hasRole('ADMIN')"
|
||||||
|
th:if="${smtpRestricted}">
|
||||||
|
<strong>This installation remains restricted until tested SMTP is active.</strong>
|
||||||
|
<a class="alert-action" th:href="@{/admin/smtp}">Configure SMTP</a>
|
||||||
|
</div>
|
||||||
<th:block th:replace="${content}"></th:block>
|
<th:block th:replace="${content}"></th:block>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
<html lang="en" xmlns:th="http://www.thymeleaf.org"
|
||||||
<head><meta charset="utf-8"><title>Defer SMTP configuration</title><link rel="icon" th:href="@{/assets/icons.svg}" type="image/svg+xml"></head>
|
th:replace="~{fragments/layout :: shell(
|
||||||
|
pageTitle='Defer SMTP configuration',
|
||||||
|
section='Admin',
|
||||||
|
activeNav='smtp',
|
||||||
|
primaryAction=~{::#primary-action},
|
||||||
|
content=~{::main})}">
|
||||||
<body>
|
<body>
|
||||||
|
<a id="primary-action" class="button button-primary" th:href="@{/admin/smtp}">Configure SMTP</a>
|
||||||
<main>
|
<main>
|
||||||
<h1>Defer SMTP configuration</h1>
|
<p class="page-description">Review each consequence before continuing without an active mail service.</p>
|
||||||
<p role="alert" th:text="${deferralWarning}"></p>
|
<section class="panel form-panel form-grid" aria-labelledby="smtp-deferral-progress">
|
||||||
<p th:text="|Confirmation ${deferralStep} of 5|"></p>
|
<p class="alert alert-warning" role="alert" th:text="${deferralWarning}"></p>
|
||||||
<a th:href="@{/admin/smtp}">Configure SMTP</a>
|
<p id="smtp-deferral-progress" th:text="|Confirmation ${deferralStep} of 5|"></p>
|
||||||
|
<div class="form-actions">
|
||||||
<form th:if="${deferralStep > 1}" method="post" th:action="@{/admin/smtp/defer/back}">
|
<form th:if="${deferralStep > 1}" method="post" th:action="@{/admin/smtp/defer/back}">
|
||||||
<button type="submit">Back</button>
|
<button class="button" type="submit">Back</button>
|
||||||
</form>
|
</form>
|
||||||
<p th:if="${deferralStep == 1}"><a th:href="@{/admin/smtp}">Back</a></p>
|
<a class="button" th:if="${deferralStep == 1}" th:href="@{/admin/smtp}">Back</a>
|
||||||
<form th:if="${deferralStep < 5}" method="post" th:action="@{/admin/smtp/defer/next}">
|
<form th:if="${deferralStep < 5}" method="post" th:action="@{/admin/smtp/defer/next}">
|
||||||
<button type="submit">I understand; continue</button>
|
<button class="button button-primary" type="submit">I understand; continue</button>
|
||||||
</form>
|
</form>
|
||||||
<form th:if="${deferralStep == 5}" method="post" th:action="@{/admin/smtp/defer/finish}">
|
<form th:if="${deferralStep == 5}" method="post" th:action="@{/admin/smtp/defer/finish}">
|
||||||
<button type="submit">Finish without SMTP</button>
|
<button class="button button-primary" type="submit">Finish without SMTP</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,45 +1,89 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
<html lang="en" xmlns:th="http://www.thymeleaf.org"
|
||||||
<head><meta charset="utf-8"><title>SMTP configuration</title><link rel="icon" th:href="@{/assets/icons.svg}" type="image/svg+xml"></head>
|
th:replace="~{fragments/layout :: shell(
|
||||||
|
pageTitle='SMTP configuration',
|
||||||
|
section='Admin',
|
||||||
|
activeNav='smtp',
|
||||||
|
primaryAction=~{::#primary-action},
|
||||||
|
content=~{::main})}">
|
||||||
<body>
|
<body>
|
||||||
|
<a id="primary-action" class="button" th:href="@{/dashboard}">Back to overview</a>
|
||||||
<main>
|
<main>
|
||||||
<h1>SMTP configuration</h1>
|
<p class="page-description" th:if="${param.onboarding}">Configure SMTP now to enable account onboarding and recovery.</p>
|
||||||
<p th:if="${smtpRestricted}" role="alert">This is a restricted installation until tested SMTP is active.</p>
|
<p class="alert" th:if="${smtpStatus.active}" role="status">SMTP is active.</p>
|
||||||
<p th:if="${param.onboarding}">Configure SMTP now to enable account onboarding and recovery.</p>
|
<p class="alert" th:if="${smtpStatus.draftId != null}" role="status">Draft saved.</p>
|
||||||
<p th:if="${smtpStatus.active}" role="status">SMTP is active.</p>
|
<p class="alert" th:if="${smtpStatus.tested}" role="status">Test passed.</p>
|
||||||
<p th:if="${smtpStatus.draftId != null}" role="status">Draft saved.</p>
|
<p class="alert alert-error" th:if="${smtpActionError}" th:text="${smtpActionError}" role="alert"></p>
|
||||||
<p th:if="${smtpStatus.tested}" role="status">Test passed.</p>
|
|
||||||
<p th:if="${smtpActionError}" th:text="${smtpActionError}" role="alert"></p>
|
<form class="panel form-panel form-grid" method="post" th:action="@{/admin/smtp/draft}" th:object="${smtpForm}">
|
||||||
<form method="post" th:action="@{/admin/smtp/draft}" th:object="${smtpForm}">
|
<div id="smtp-form-error-summary" class="alert alert-error" role="alert"
|
||||||
<div th:if="${#fields.hasGlobalErrors()}" role="alert">
|
aria-labelledby="smtp-form-error-summary-title" th:if="${#fields.hasAnyErrors()}">
|
||||||
<p th:each="error : ${#fields.globalErrors()}" th:text="${error}"></p>
|
<strong id="smtp-form-error-summary-title">Please correct the highlighted SMTP settings.</strong>
|
||||||
|
<ul><li th:each="fieldError : ${#fields.allErrors()}" th:text="${fieldError}"></li></ul>
|
||||||
</div>
|
</div>
|
||||||
<label>Host <input th:field="*{host}" required></label>
|
<div class="form-grid form-grid-three">
|
||||||
<p th:if="${#fields.hasErrors('host')}" th:errors="*{host}" role="alert"></p>
|
<div class="field">
|
||||||
<label>Port <input th:field="*{port}" type="number" min="1" max="65535" required></label>
|
<label class="field-label" for="smtp-host">Host</label>
|
||||||
<p th:if="${#fields.hasErrors('port')}" th:errors="*{port}" role="alert"></p>
|
<input class="control" id="smtp-host" th:field="*{host}" required
|
||||||
<label>Security <select th:field="*{securityMode}"><option value="STARTTLS">STARTTLS</option><option value="TLS">TLS</option><option value="NONE">NONE</option></select></label>
|
th:attr="aria-invalid=${#fields.hasErrors('host')},aria-describedby=${#fields.hasErrors('host') ? 'smtp-host-error' : null}">
|
||||||
<p th:if="${#fields.hasErrors('securityMode')}" th:errors="*{securityMode}" role="alert"></p>
|
<p class="field-error" id="smtp-host-error" th:if="${#fields.hasErrors('host')}" th:errors="*{host}" role="alert"></p>
|
||||||
<label>Username <input th:field="*{username}" autocomplete="username"></label>
|
</div>
|
||||||
<label>Password <input name="password" type="password" autocomplete="new-password"></label>
|
<div class="field">
|
||||||
<p th:if="${#fields.hasErrors('authenticationComplete')}" th:errors="*{authenticationComplete}" role="alert"></p>
|
<label class="field-label" for="smtp-port">Port</label>
|
||||||
<label>From address <input th:field="*{fromAddress}" type="email" required></label>
|
<input class="control" id="smtp-port" th:field="*{port}" type="number" min="1" max="65535" required
|
||||||
<p th:if="${#fields.hasErrors('fromAddress')}" th:errors="*{fromAddress}" role="alert"></p>
|
th:attr="aria-invalid=${#fields.hasErrors('port')},aria-describedby=${#fields.hasErrors('port') ? 'smtp-port-error' : null}">
|
||||||
<label>From name <input th:field="*{fromName}" required></label>
|
<p class="field-error" id="smtp-port-error" th:if="${#fields.hasErrors('port')}" th:errors="*{port}" role="alert"></p>
|
||||||
<p th:if="${#fields.hasErrors('fromName')}" th:errors="*{fromName}" role="alert"></p>
|
</div>
|
||||||
<button type="submit">Save draft</button>
|
<div class="field">
|
||||||
|
<label class="field-label" for="smtp-security-mode">Security</label>
|
||||||
|
<select class="control" id="smtp-security-mode" th:field="*{securityMode}"
|
||||||
|
th:attr="aria-invalid=${#fields.hasErrors('securityMode')},aria-describedby=${#fields.hasErrors('securityMode') ? 'smtp-security-mode-error' : null}">
|
||||||
|
<option value="STARTTLS">STARTTLS</option>
|
||||||
|
<option value="TLS">TLS</option>
|
||||||
|
<option value="NONE">NONE</option>
|
||||||
|
</select>
|
||||||
|
<p class="field-error" id="smtp-security-mode-error" th:if="${#fields.hasErrors('securityMode')}" th:errors="*{securityMode}" role="alert"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid form-grid-three">
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="smtp-username">Username</label>
|
||||||
|
<input class="control" id="smtp-username" th:field="*{username}" autocomplete="username"
|
||||||
|
th:attr="aria-invalid=${#fields.hasErrors('authenticationComplete')},aria-describedby=${#fields.hasErrors('authenticationComplete') ? 'smtp-authentication-error' : null}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="smtp-password">Password</label>
|
||||||
|
<input class="control" id="smtp-password" name="password" type="password" autocomplete="new-password"
|
||||||
|
th:attr="aria-invalid=${#fields.hasErrors('authenticationComplete')},aria-describedby=${#fields.hasErrors('authenticationComplete') ? 'smtp-authentication-error' : null}">
|
||||||
|
<p class="field-error" id="smtp-authentication-error" th:if="${#fields.hasErrors('authenticationComplete')}" th:errors="*{authenticationComplete}" role="alert"></p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="smtp-from-address">From address</label>
|
||||||
|
<input class="control" id="smtp-from-address" th:field="*{fromAddress}" type="email" required
|
||||||
|
th:attr="aria-invalid=${#fields.hasErrors('fromAddress')},aria-describedby=${#fields.hasErrors('fromAddress') ? 'smtp-from-address-error' : null}">
|
||||||
|
<p class="field-error" id="smtp-from-address-error" th:if="${#fields.hasErrors('fromAddress')}" th:errors="*{fromAddress}" role="alert"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="smtp-from-name">From name</label>
|
||||||
|
<input class="control" id="smtp-from-name" th:field="*{fromName}" required
|
||||||
|
th:attr="aria-invalid=${#fields.hasErrors('fromName')},aria-describedby=${#fields.hasErrors('fromName') ? 'smtp-from-name-error' : null}">
|
||||||
|
<p class="field-error" id="smtp-from-name-error" th:if="${#fields.hasErrors('fromName')}" th:errors="*{fromName}" role="alert"></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions"><button class="button button-primary" type="submit">Save draft</button></div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="inline-actions">
|
||||||
<form th:if="${smtpStatus.draftId != null}" method="post" th:action="@{/admin/smtp/test}">
|
<form th:if="${smtpStatus.draftId != null}" method="post" th:action="@{/admin/smtp/test}">
|
||||||
<input type="hidden" name="draftId" th:value="${smtpStatus.draftId}">
|
<input type="hidden" name="draftId" th:value="${smtpStatus.draftId}">
|
||||||
<button type="submit">Test connection</button>
|
<button class="button" type="submit">Test connection</button>
|
||||||
</form>
|
</form>
|
||||||
<form th:if="${smtpStatus.draftId != null and smtpStatus.tested}" method="post" th:action="@{/admin/smtp/activate}">
|
<form th:if="${smtpStatus.draftId != null and smtpStatus.tested}" method="post" th:action="@{/admin/smtp/activate}">
|
||||||
<input type="hidden" name="draftId" th:value="${smtpStatus.draftId}">
|
<input type="hidden" name="draftId" th:value="${smtpStatus.draftId}">
|
||||||
<button type="submit">Activate SMTP</button>
|
<button class="button button-primary" type="submit">Activate SMTP</button>
|
||||||
</form>
|
</form>
|
||||||
<p th:if="${param.onboarding}">
|
<a class="button" th:if="${param.onboarding}" th:href="@{/admin/smtp/defer}">Defer SMTP</a>
|
||||||
<a th:href="@{/admin/smtp/defer}">Defer SMTP</a>
|
</div>
|
||||||
</p>
|
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+16
@@ -71,6 +71,8 @@ class BootstrapOnboardingWebIntegrationTest {
|
|||||||
.with(user("admin@example.com").roles("ADMIN")))
|
.with(user("admin@example.com").roles("ADMIN")))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(view().name("smtp/defer"))
|
.andExpect(view().name("smtp/defer"))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("/assets/theme.js")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("data-sidebar-toggle")))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Account onboarding is disabled")))
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("Account onboarding is disabled")))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Back")))
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("Back")))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Configure SMTP")))
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("Configure SMTP")))
|
||||||
@@ -91,6 +93,20 @@ class BootstrapOnboardingWebIntegrationTest {
|
|||||||
.with(csrf()))
|
.with(csrf()))
|
||||||
.andExpect(status().is3xxRedirection())
|
.andExpect(status().is3xxRedirection())
|
||||||
.andExpect(redirectedUrl("/dashboard"));
|
.andExpect(redirectedUrl("/dashboard"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/dashboard")
|
||||||
|
.with(user("admin@example.com").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.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\"")));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/admin/accounts/new")
|
||||||
|
.with(user("admin@example.com").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.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\"")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+65
-4
@@ -102,6 +102,11 @@ class SmtpOnboardingWebIntegrationTest {
|
|||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("SMTP is active")))
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("SMTP is active")))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.not(
|
.andExpect(content().string(org.hamcrest.Matchers.not(
|
||||||
org.hamcrest.Matchers.containsString("restricted installation"))));
|
org.hamcrest.Matchers.containsString("restricted installation"))));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/admin/accounts/new").with(user("admin@example.com").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.not(
|
||||||
|
org.hamcrest.Matchers.containsString("installation remains restricted"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -111,16 +116,71 @@ class SmtpOnboardingWebIntegrationTest {
|
|||||||
.with(csrf())
|
.with(csrf())
|
||||||
.param("host", "")
|
.param("host", "")
|
||||||
.param("port", "70000")
|
.param("port", "70000")
|
||||||
.param("securityMode", "STARTTLS")
|
.param("securityMode", "")
|
||||||
.param("username", "smtp-user")
|
.param("username", "smtp-user")
|
||||||
.param("password", "must-not-be-rendered")
|
.param("password", "")
|
||||||
.param("fromAddress", "not-an-email")
|
.param("fromAddress", "not-an-email")
|
||||||
.param("fromName", "Safe sender name"))
|
.param("fromName", ""))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(view().name("smtp/form"))
|
.andExpect(view().name("smtp/form"))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("/assets/theme.js")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-form-error-summary\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"aria-labelledby=\"smtp-form-error-summary-title\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-host\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"aria-describedby=\"smtp-host-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-host-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-port\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"aria-describedby=\"smtp-port-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-port-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-security-mode\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"aria-describedby=\"smtp-security-mode-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"id=\"smtp-security-mode-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-username\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-password\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"aria-describedby=\"smtp-authentication-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"id=\"smtp-username\" autocomplete=\"username\" aria-invalid=\"true\""
|
||||||
|
+ " aria-describedby=\"smtp-authentication-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"id=\"smtp-authentication-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-from-address\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"aria-describedby=\"smtp-from-address-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"id=\"smtp-from-address-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("id=\"smtp-from-name\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"aria-describedby=\"smtp-from-name-error\"")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"id=\"smtp-from-name-error\"")))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Host is required")))
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("Host is required")))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Port must be between")))
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("Port must be between")))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("valid email address")))
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("valid email address")))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.not(
|
||||||
|
org.hamcrest.Matchers.containsString("value=\"smtp-secret\""))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidDraftRetainsSafeValuesButNeverTheSubmittedPassword() throws Exception {
|
||||||
|
mockMvc.perform(post("/admin/smtp/draft")
|
||||||
|
.with(user("admin@example.com").roles("ADMIN"))
|
||||||
|
.with(csrf())
|
||||||
|
.param("host", "")
|
||||||
|
.param("port", "1025")
|
||||||
|
.param("securityMode", "NONE")
|
||||||
|
.param("username", "safe-smtp-user")
|
||||||
|
.param("password", "must-not-be-rendered")
|
||||||
|
.param("fromAddress", "notifications@example.com")
|
||||||
|
.param("fromName", "Safe sender name"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("smtp/form"))
|
||||||
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("safe-smtp-user")))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Safe sender name")))
|
.andExpect(content().string(org.hamcrest.Matchers.containsString("Safe sender name")))
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.not(
|
.andExpect(content().string(org.hamcrest.Matchers.not(
|
||||||
org.hamcrest.Matchers.containsString("must-not-be-rendered"))));
|
org.hamcrest.Matchers.containsString("must-not-be-rendered"))));
|
||||||
@@ -130,7 +190,8 @@ class SmtpOnboardingWebIntegrationTest {
|
|||||||
void restrictedWarningPersistsOnAdminPagesUntilActivationAndMutationsRequireCsrf() throws Exception {
|
void restrictedWarningPersistsOnAdminPagesUntilActivationAndMutationsRequireCsrf() throws Exception {
|
||||||
mockMvc.perform(get("/admin/accounts/new").with(user("admin@example.com").roles("ADMIN")))
|
mockMvc.perform(get("/admin/accounts/new").with(user("admin@example.com").roles("ADMIN")))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("restricted installation")));
|
.andExpect(content().string(org.hamcrest.Matchers.containsString(
|
||||||
|
"installation remains restricted")));
|
||||||
|
|
||||||
mockMvc.perform(post("/admin/smtp/draft")
|
mockMvc.perform(post("/admin/smtp/draft")
|
||||||
.with(user("admin@example.com").roles("ADMIN"))
|
.with(user("admin@example.com").roles("ADMIN"))
|
||||||
|
|||||||
+21
-2
@@ -6,6 +6,7 @@ import static org.mockito.Mockito.verifyNoInteractions;
|
|||||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
|
||||||
|
|
||||||
@@ -35,15 +36,31 @@ class DashboardControllerWebTest {
|
|||||||
void adminRendersAdminDashboardForAuthenticatedIdentity() throws Exception {
|
void adminRendersAdminDashboardForAuthenticatedIdentity() throws Exception {
|
||||||
var dashboard = new DashboardView.Admin(2, 1, 1, 3);
|
var dashboard = new DashboardView.Admin(2, 1, 1, 3);
|
||||||
given(dashboards.admin("admin@example.test")).willReturn(dashboard);
|
given(dashboards.admin("admin@example.test")).willReturn(dashboard);
|
||||||
|
given(smtpConfiguration.hasActiveConfiguration()).willReturn(false);
|
||||||
|
|
||||||
mvc.perform(get("/dashboard").with(user("admin@example.test").roles("ADMIN")))
|
mvc.perform(get("/dashboard").with(user("admin@example.test").roles("ADMIN")))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(view().name("dashboard/admin"))
|
.andExpect(view().name("dashboard/admin"))
|
||||||
.andExpect(model().attribute("dashboard", dashboard));
|
.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\"")));
|
||||||
|
|
||||||
verify(dashboards).admin("admin@example.test");
|
verify(dashboards).admin("admin@example.test");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void activeSmtpKeepsAdminDashboardFreeOfTheRestrictedInstallationWarning() throws Exception {
|
||||||
|
var dashboard = new DashboardView.Admin(2, 1, 1, 3);
|
||||||
|
given(dashboards.admin("admin@example.test")).willReturn(dashboard);
|
||||||
|
given(smtpConfiguration.hasActiveConfiguration()).willReturn(true);
|
||||||
|
|
||||||
|
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."))));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void mentorRendersMentorDashboardForAuthenticatedIdentity() throws Exception {
|
void mentorRendersMentorDashboardForAuthenticatedIdentity() throws Exception {
|
||||||
var dashboard = new DashboardView.Mentor("Mentor", 2, 4, 1);
|
var dashboard = new DashboardView.Mentor("Mentor", 2, 4, 1);
|
||||||
@@ -52,7 +69,9 @@ class DashboardControllerWebTest {
|
|||||||
mvc.perform(get("/dashboard").with(user("mentor@example.test").roles("MENTOR")))
|
mvc.perform(get("/dashboard").with(user("mentor@example.test").roles("MENTOR")))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(view().name("dashboard/mentor"))
|
.andExpect(view().name("dashboard/mentor"))
|
||||||
.andExpect(model().attribute("dashboard", dashboard));
|
.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."))));
|
||||||
|
|
||||||
verify(dashboards).mentor("mentor@example.test");
|
verify(dashboards).mentor("mentor@example.test");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user