From ddf688a5336421762ff970499bafb09505474fca Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 04:33:37 +0700 Subject: [PATCH] fix(ui): persist SMTP restriction in shared shell --- docs/tests/web/review-round-2-smtp-shell.md | 87 +++++++++++++ src/main/frontend/app.css | 2 + src/main/resources/static/assets/app.css | 2 +- .../resources/templates/accounts/new.html | 1 - .../resources/templates/fragments/layout.html | 5 + src/main/resources/templates/smtp/defer.html | 41 +++--- src/main/resources/templates/smtp/form.html | 118 ++++++++++++------ ...BootstrapOnboardingWebIntegrationTest.java | 16 +++ .../SmtpOnboardingWebIntegrationTest.java | 69 +++++++++- .../DashboardControllerWebTest.java | 23 +++- 10 files changed, 303 insertions(+), 61 deletions(-) create mode 100644 docs/tests/web/review-round-2-smtp-shell.md diff --git a/docs/tests/web/review-round-2-smtp-shell.md b/docs/tests/web/review-round-2-smtp-shell.md new file mode 100644 index 0000000..570dd75 --- /dev/null +++ b/docs/tests/web/review-round-2-smtp-shell.md @@ -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. diff --git a/src/main/frontend/app.css b/src/main/frontend/app.css index e804c96..7fd935b 100644 --- a/src/main/frontend/app.css +++ b/src/main/frontend/app.css @@ -152,6 +152,8 @@ .badge-warning { color: var(--warning); } .badge-danger { color: var(--danger); } .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); } .empty-state { padding: 2.5rem 1rem; text-align: center; } .empty-state p { margin: .3rem auto 0; color: var(--muted); } diff --git a/src/main/resources/static/assets/app.css b/src/main/resources/static/assets/app.css index ad91f4e..9bf6130 100644 --- a/src/main/resources/static/assets/app.css +++ b/src/main/resources/static/assets/app.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ -@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{box-sizing:border-box}html{background:var(--canvas);min-width:64rem}body{background:var(--canvas);color:var(--ink);margin:0;font:14px/1.45 ui-sans-serif,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;overflow-x:hidden}button,input,select,textarea{font:inherit}button,a,input,select,textarea{outline:none}:focus-visible{outline:3px solid var(--focus);outline-offset:2px}a{color:inherit}}@layer components{.app-shell{grid-template-columns:16rem minmax(0,1fr);min-height:100vh;display:grid}.auth-shell{min-height:100vh}.auth-header{border-bottom:1px solid var(--border);justify-content:space-between;align-items:center;min-height:4rem;padding:.75rem 1.25rem;display:flex}.auth-theme{width:9rem}.auth-main{place-items:center;min-height:calc(100vh - 4rem);padding:2rem;display:grid}.auth-card{border:1px solid var(--border);background:var(--panel);border-radius:.85rem;width:min(100%,28rem);padding:1.5rem;box-shadow:0 16px 42px #14192314}.auth-eyebrow{color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin:0 0 .35rem;font-size:.72rem;font-weight:750}.auth-form{margin-top:1.25rem}[data-sidebar-collapsed=true] .app-shell{grid-template-columns:4rem minmax(0,1fr)}.sidebar{border-right:1px solid var(--border);background:var(--sidebar);flex-direction:column;height:100vh;padding:1rem .75rem;display:flex;position:sticky;top:0}.brand,.account{align-items:center;gap:.7rem;min-width:0;padding:.25rem .4rem;display:flex}.brand-mark{background:var(--ink);width:2rem;height:2rem;color:var(--panel);border-radius:.55rem;flex:none;place-items:center;display:grid}.sidebar-label{white-space:nowrap;overflow:hidden}[data-sidebar-collapsed=true] .sidebar-label{opacity:0;width:0}.nav-label{color:var(--subtle);letter-spacing:.08em;text-transform:uppercase;margin:1.6rem .6rem .4rem;font-size:.68rem;font-weight:750}.nav-list{gap:.2rem;margin:0;padding:0;list-style:none;display:grid}.nav-link{min-height:2.5rem;color:var(--muted);border-radius:.55rem;align-items:center;gap:.7rem;padding:.55rem .7rem;font-weight:600;text-decoration:none;display:flex}.nav-link:hover,.nav-link[aria-current=page]{background:var(--panel);color:var(--ink);box-shadow:0 1px 2px #14192314}.nav-link[data-tooltip]{position:relative}[data-sidebar-collapsed=true] .nav-link[data-tooltip]:hover:after,[data-sidebar-collapsed=true] .nav-link[data-tooltip]:focus-visible:after{z-index:20;border:1px solid var(--border-strong);background:var(--ink);color:var(--panel);content:attr(data-tooltip);pointer-events:none;white-space:nowrap;border-radius:.4rem;padding:.38rem .55rem;font-size:.75rem;line-height:1;position:absolute;top:50%;left:calc(100% + .75rem);transform:translateY(-50%)}.nav-icon{flex:none;width:1.05rem;height:1.05rem}.sidebar-footer{gap:.7rem;margin-top:auto;display:grid}.theme-field{gap:.25rem;display:grid}.theme-field select{border:1px solid var(--border-strong);background:var(--panel);min-height:2.4rem;color:var(--ink);border-radius:.5rem;padding:.35rem .55rem}[data-sidebar-collapsed=true] .theme-field select{width:2.5rem;padding-inline:.25rem;font-size:0}.logout-form button{text-align:left;background:0 0;border:0;width:100%}.app-column{min-width:0}.app-header{border-bottom:1px solid var(--border);align-items:center;gap:.8rem;min-height:3.75rem;padding:0 1.5rem;display:flex}.header-title{min-width:0;font-weight:700}.breadcrumb{color:var(--muted);font-weight:400}.header-actions{align-items:center;gap:.55rem;margin-left:auto;display:flex}.icon-button{border:1px solid var(--border-strong);background:var(--panel);width:2.5rem;height:2.5rem;color:var(--ink);cursor:pointer;border-radius:.5rem;place-items:center;display:inline-grid}.page{min-width:0;padding:1.55rem}.page-heading{align-items:end;gap:1rem;margin-bottom:1.1rem;display:flex}.page-heading-copy{min-width:0}.page-title{letter-spacing:-.025em;margin:0;font-size:1.56rem;line-height:1.2}.page-description{max-width:72ch;color:var(--muted);margin:.3rem 0 0}.primary-action{margin-left:auto}.button{border:1px solid var(--border-strong);background:var(--panel);min-height:2.35rem;color:var(--ink);cursor:pointer;border-radius:.5rem;justify-content:center;align-items:center;gap:.45rem;padding:.5rem .8rem;font-weight:650;text-decoration:none;display:inline-flex}.button-primary{border-color:var(--ink);background:var(--ink);color:var(--panel)}.button-danger{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.button-danger{border-color:color-mix(in srgb, var(--danger), transparent 65%)}}.button-danger{background:var(--danger)}@supports (color:color-mix(in lab, red, red)){.button-danger{background:color-mix(in srgb, var(--danger), transparent 90%)}}.button-danger{color:var(--danger)}.panel{border:1px solid var(--border);background:var(--panel);border-radius:.75rem;box-shadow:0 10px 28px #1419230f}.panel-header{border-bottom:1px solid var(--border);padding:.9rem 1rem}.panel-title{margin:0;font-size:1rem}.metric-strip{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:1rem;display:grid;overflow:hidden}.metric-strip-three{grid-template-columns:repeat(3,minmax(0,1fr))}.metric{min-width:0;padding:1rem}.metric+.metric{border-left:1px solid var(--border)}.metric-label{color:var(--muted);font-size:.78rem}.metric-value{font-variant-numeric:tabular-nums;margin-top:.35rem;font-size:1.4rem;font-weight:700}.metric-detail{color:var(--muted);margin-top:.18rem;font-size:.78rem}.field{gap:.35rem;display:grid}.form-panel{margin-top:1rem;padding:1rem}.form-grid{gap:1rem;display:grid}.form-grid-three{grid-template-columns:repeat(3,minmax(0,1fr))}.form-section{border:1px solid var(--border);border-radius:.65rem;padding:1rem}.form-section legend{padding:0 .35rem;font-weight:700}.field-help{color:var(--muted);margin:0 0 .8rem;font-size:.78rem}.form-actions{justify-content:flex-end;gap:.6rem;display:flex}.inline-actions{gap:.6rem;margin:1rem 0;display:flex}.filter-form{grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;align-items:end;gap:.8rem;margin:1rem 0;display:grid}.field-label{font-size:.78rem;font-weight:650}.control{border:1px solid var(--border-strong);background:var(--panel);width:100%;min-height:2.45rem;color:var(--ink);border-radius:.5rem;padding:.55rem .65rem}.control[aria-invalid=true]{border-color:var(--danger)}.field-error{color:var(--danger);margin:0;font-size:.78rem}.checkbox{align-items:center;gap:.5rem;display:flex}.badge{border:1px solid var(--border);border-radius:999px;align-items:center;gap:.32rem;padding:.15rem .45rem;font-size:.72rem;font-weight:700;display:inline-flex}.badge:before{content:"";background:currentColor;border-radius:50%;width:.38rem;height:.38rem}.badge-success{color:var(--success)}.badge-warning{color:var(--warning)}.badge-danger{color:var(--danger)}.alert{border:1px solid var(--border);border-radius:.6rem;margin:.75rem 0;padding:.75rem .9rem}.alert-error{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.alert-error{border-color:color-mix(in srgb, var(--danger), transparent 60%)}}.alert-error{color:var(--danger)}.empty-state{text-align:center;padding:2.5rem 1rem}.empty-state p{color:var(--muted);margin:.3rem auto 0}.table-scroll{max-width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;min-width:42rem}.data-table th{background:var(--panel-muted);color:var(--muted);letter-spacing:.06em;text-align:left;text-transform:uppercase;font-size:.69rem}.data-table th,.data-table td{border-bottom:1px solid var(--border);padding:.7rem 1rem}.data-table tr:last-child td{border-bottom:0}.tabs{border:1px solid var(--border);background:var(--panel-muted);border-radius:.55rem;gap:.2rem;padding:.2rem;display:inline-flex}.tab{border-radius:.4rem;padding:.4rem .65rem;text-decoration:none}.tab[aria-current=page]{background:var(--panel);box-shadow:0 1px 2px #14192314}.pagination{justify-content:flex-end;align-items:center;gap:.4rem;padding:.8rem 1rem;display:flex}.skeleton{background:var(--panel-muted);border-radius:.35rem;height:1rem;animation:1.5s ease-in-out infinite pulse}.notification-menu{min-width:18rem;padding:.75rem}dialog{border:1px solid var(--border);background:var(--panel);max-width:30rem;color:var(--ink);border-radius:.9rem;padding:1.25rem}dialog::backdrop{background:#00000073}@keyframes pulse{50%{opacity:.45}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;animation-duration:.01ms!important}}}@layer utilities{.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.hidden{display:none}}:root{color-scheme:light;--ink:#15171a;--canvas:#f6f7f8;--sidebar:#f0f1f2;--panel:#fff;--panel-muted:#f7f8f9;--border:#858c96;--border-strong:#747d89;--muted:#626a75;--subtle:#626a75;--accent:#3157e7;--focus:#3157e7;--success:#087a48;--warning:#7a4d00;--danger:#b42318}:root[data-theme=dark]{color-scheme:dark;--ink:#eceef1;--canvas:#0b0c0e;--sidebar:#111317;--panel:#17191e;--panel-muted:#1d2026;--border:#626b78;--border-strong:#707987;--muted:#b2b7c0;--subtle:#969da8;--accent:#8ca4ff;--focus:#9eb2ff;--success:#4fd19b;--warning:#f0bc63;--danger:#ff8e88}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file +@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{box-sizing:border-box}html{background:var(--canvas);min-width:64rem}body{background:var(--canvas);color:var(--ink);margin:0;font:14px/1.45 ui-sans-serif,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;overflow-x:hidden}button,input,select,textarea{font:inherit}button,a,input,select,textarea{outline:none}:focus-visible{outline:3px solid var(--focus);outline-offset:2px}a{color:inherit}}@layer components{.app-shell{grid-template-columns:16rem minmax(0,1fr);min-height:100vh;display:grid}.auth-shell{min-height:100vh}.auth-header{border-bottom:1px solid var(--border);justify-content:space-between;align-items:center;min-height:4rem;padding:.75rem 1.25rem;display:flex}.auth-theme{width:9rem}.auth-main{place-items:center;min-height:calc(100vh - 4rem);padding:2rem;display:grid}.auth-card{border:1px solid var(--border);background:var(--panel);border-radius:.85rem;width:min(100%,28rem);padding:1.5rem;box-shadow:0 16px 42px #14192314}.auth-eyebrow{color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin:0 0 .35rem;font-size:.72rem;font-weight:750}.auth-form{margin-top:1.25rem}[data-sidebar-collapsed=true] .app-shell{grid-template-columns:4rem minmax(0,1fr)}.sidebar{border-right:1px solid var(--border);background:var(--sidebar);flex-direction:column;height:100vh;padding:1rem .75rem;display:flex;position:sticky;top:0}.brand,.account{align-items:center;gap:.7rem;min-width:0;padding:.25rem .4rem;display:flex}.brand-mark{background:var(--ink);width:2rem;height:2rem;color:var(--panel);border-radius:.55rem;flex:none;place-items:center;display:grid}.sidebar-label{white-space:nowrap;overflow:hidden}[data-sidebar-collapsed=true] .sidebar-label{opacity:0;width:0}.nav-label{color:var(--subtle);letter-spacing:.08em;text-transform:uppercase;margin:1.6rem .6rem .4rem;font-size:.68rem;font-weight:750}.nav-list{gap:.2rem;margin:0;padding:0;list-style:none;display:grid}.nav-link{min-height:2.5rem;color:var(--muted);border-radius:.55rem;align-items:center;gap:.7rem;padding:.55rem .7rem;font-weight:600;text-decoration:none;display:flex}.nav-link:hover,.nav-link[aria-current=page]{background:var(--panel);color:var(--ink);box-shadow:0 1px 2px #14192314}.nav-link[data-tooltip]{position:relative}[data-sidebar-collapsed=true] .nav-link[data-tooltip]:hover:after,[data-sidebar-collapsed=true] .nav-link[data-tooltip]:focus-visible:after{z-index:20;border:1px solid var(--border-strong);background:var(--ink);color:var(--panel);content:attr(data-tooltip);pointer-events:none;white-space:nowrap;border-radius:.4rem;padding:.38rem .55rem;font-size:.75rem;line-height:1;position:absolute;top:50%;left:calc(100% + .75rem);transform:translateY(-50%)}.nav-icon{flex:none;width:1.05rem;height:1.05rem}.sidebar-footer{gap:.7rem;margin-top:auto;display:grid}.theme-field{gap:.25rem;display:grid}.theme-field select{border:1px solid var(--border-strong);background:var(--panel);min-height:2.4rem;color:var(--ink);border-radius:.5rem;padding:.35rem .55rem}[data-sidebar-collapsed=true] .theme-field select{width:2.5rem;padding-inline:.25rem;font-size:0}.logout-form button{text-align:left;background:0 0;border:0;width:100%}.app-column{min-width:0}.app-header{border-bottom:1px solid var(--border);align-items:center;gap:.8rem;min-height:3.75rem;padding:0 1.5rem;display:flex}.header-title{min-width:0;font-weight:700}.breadcrumb{color:var(--muted);font-weight:400}.header-actions{align-items:center;gap:.55rem;margin-left:auto;display:flex}.icon-button{border:1px solid var(--border-strong);background:var(--panel);width:2.5rem;height:2.5rem;color:var(--ink);cursor:pointer;border-radius:.5rem;place-items:center;display:inline-grid}.page{min-width:0;padding:1.55rem}.page-heading{align-items:end;gap:1rem;margin-bottom:1.1rem;display:flex}.page-heading-copy{min-width:0}.page-title{letter-spacing:-.025em;margin:0;font-size:1.56rem;line-height:1.2}.page-description{max-width:72ch;color:var(--muted);margin:.3rem 0 0}.primary-action{margin-left:auto}.button{border:1px solid var(--border-strong);background:var(--panel);min-height:2.35rem;color:var(--ink);cursor:pointer;border-radius:.5rem;justify-content:center;align-items:center;gap:.45rem;padding:.5rem .8rem;font-weight:650;text-decoration:none;display:inline-flex}.button-primary{border-color:var(--ink);background:var(--ink);color:var(--panel)}.button-danger{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.button-danger{border-color:color-mix(in srgb, var(--danger), transparent 65%)}}.button-danger{background:var(--danger)}@supports (color:color-mix(in lab, red, red)){.button-danger{background:color-mix(in srgb, var(--danger), transparent 90%)}}.button-danger{color:var(--danger)}.panel{border:1px solid var(--border);background:var(--panel);border-radius:.75rem;box-shadow:0 10px 28px #1419230f}.panel-header{border-bottom:1px solid var(--border);padding:.9rem 1rem}.panel-title{margin:0;font-size:1rem}.metric-strip{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:1rem;display:grid;overflow:hidden}.metric-strip-three{grid-template-columns:repeat(3,minmax(0,1fr))}.metric{min-width:0;padding:1rem}.metric+.metric{border-left:1px solid var(--border)}.metric-label{color:var(--muted);font-size:.78rem}.metric-value{font-variant-numeric:tabular-nums;margin-top:.35rem;font-size:1.4rem;font-weight:700}.metric-detail{color:var(--muted);margin-top:.18rem;font-size:.78rem}.field{gap:.35rem;display:grid}.form-panel{margin-top:1rem;padding:1rem}.form-grid{gap:1rem;display:grid}.form-grid-three{grid-template-columns:repeat(3,minmax(0,1fr))}.form-section{border:1px solid var(--border);border-radius:.65rem;padding:1rem}.form-section legend{padding:0 .35rem;font-weight:700}.field-help{color:var(--muted);margin:0 0 .8rem;font-size:.78rem}.form-actions{justify-content:flex-end;gap:.6rem;display:flex}.inline-actions{gap:.6rem;margin:1rem 0;display:flex}.filter-form{grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;align-items:end;gap:.8rem;margin:1rem 0;display:grid}.field-label{font-size:.78rem;font-weight:650}.control{border:1px solid var(--border-strong);background:var(--panel);width:100%;min-height:2.45rem;color:var(--ink);border-radius:.5rem;padding:.55rem .65rem}.control[aria-invalid=true]{border-color:var(--danger)}.field-error{color:var(--danger);margin:0;font-size:.78rem}.checkbox{align-items:center;gap:.5rem;display:flex}.badge{border:1px solid var(--border);border-radius:999px;align-items:center;gap:.32rem;padding:.15rem .45rem;font-size:.72rem;font-weight:700;display:inline-flex}.badge:before{content:"";background:currentColor;border-radius:50%;width:.38rem;height:.38rem}.badge-success{color:var(--success)}.badge-warning{color:var(--warning)}.badge-danger{color:var(--danger)}.alert{border:1px solid var(--border);border-radius:.6rem;margin:.75rem 0;padding:.75rem .9rem}.alert-warning{border-color:var(--warning)}@supports (color:color-mix(in lab, red, red)){.alert-warning{border-color:color-mix(in srgb, var(--warning), transparent 55%)}}.alert-warning{color:var(--warning)}.alert-action{margin-left:.6rem;font-weight:700}.alert-error{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.alert-error{border-color:color-mix(in srgb, var(--danger), transparent 60%)}}.alert-error{color:var(--danger)}.empty-state{text-align:center;padding:2.5rem 1rem}.empty-state p{color:var(--muted);margin:.3rem auto 0}.table-scroll{max-width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;min-width:42rem}.data-table th{background:var(--panel-muted);color:var(--muted);letter-spacing:.06em;text-align:left;text-transform:uppercase;font-size:.69rem}.data-table th,.data-table td{border-bottom:1px solid var(--border);padding:.7rem 1rem}.data-table tr:last-child td{border-bottom:0}.tabs{border:1px solid var(--border);background:var(--panel-muted);border-radius:.55rem;gap:.2rem;padding:.2rem;display:inline-flex}.tab{border-radius:.4rem;padding:.4rem .65rem;text-decoration:none}.tab[aria-current=page]{background:var(--panel);box-shadow:0 1px 2px #14192314}.pagination{justify-content:flex-end;align-items:center;gap:.4rem;padding:.8rem 1rem;display:flex}.skeleton{background:var(--panel-muted);border-radius:.35rem;height:1rem;animation:1.5s ease-in-out infinite pulse}.notification-menu{min-width:18rem;padding:.75rem}dialog{border:1px solid var(--border);background:var(--panel);max-width:30rem;color:var(--ink);border-radius:.9rem;padding:1.25rem}dialog::backdrop{background:#00000073}@keyframes pulse{50%{opacity:.45}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;animation-duration:.01ms!important}}}@layer utilities{.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.hidden{display:none}}:root{color-scheme:light;--ink:#15171a;--canvas:#f6f7f8;--sidebar:#f0f1f2;--panel:#fff;--panel-muted:#f7f8f9;--border:#858c96;--border-strong:#747d89;--muted:#626a75;--subtle:#626a75;--accent:#3157e7;--focus:#3157e7;--success:#087a48;--warning:#7a4d00;--danger:#b42318}:root[data-theme=dark]{color-scheme:dark;--ink:#eceef1;--canvas:#0b0c0e;--sidebar:#111317;--panel:#17191e;--panel-muted:#1d2026;--border:#626b78;--border-strong:#707987;--muted:#b2b7c0;--subtle:#969da8;--accent:#8ca4ff;--focus:#9eb2ff;--success:#4fd19b;--warning:#f0bc63;--danger:#ff8e88}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file diff --git a/src/main/resources/templates/accounts/new.html b/src/main/resources/templates/accounts/new.html index 1e79c38..3e88074 100644 --- a/src/main/resources/templates/accounts/new.html +++ b/src/main/resources/templates/accounts/new.html @@ -10,7 +10,6 @@ Back to overview

Create a role-specific account and send its one-time activation link.

-

Account created and activation email sent.

diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html index 4de10be..37bae1a 100644 --- a/src/main/resources/templates/fragments/layout.html +++ b/src/main/resources/templates/fragments/layout.html @@ -52,6 +52,11 @@

Page

+ diff --git a/src/main/resources/templates/smtp/defer.html b/src/main/resources/templates/smtp/defer.html index 17abefa..2f43c1c 100644 --- a/src/main/resources/templates/smtp/defer.html +++ b/src/main/resources/templates/smtp/defer.html @@ -1,22 +1,31 @@ - -Defer SMTP configuration + +Configure SMTP
-

Defer SMTP configuration

-

-

- Configure SMTP - - - -

Back

-
- -
-
- -
+

Review each consequence before continuing without an active mail service.

+
+ +

+
+
+ +
+ Back +
+ +
+
+ +
+
+
diff --git a/src/main/resources/templates/smtp/form.html b/src/main/resources/templates/smtp/form.html index ddb16c8..bfeff6f 100644 --- a/src/main/resources/templates/smtp/form.html +++ b/src/main/resources/templates/smtp/form.html @@ -1,45 +1,89 @@ - -SMTP configuration + +Back to overview
-

SMTP configuration

-

This is a restricted installation until tested SMTP is active.

-

Configure SMTP now to enable account onboarding and recovery.

-

SMTP is active.

-

Draft saved.

-

Test passed.

-

-
-
-

+

Configure SMTP now to enable account onboarding and recovery.

+

SMTP is active.

+

Draft saved.

+

Test passed.

+ + + + - -

- -

- -

- - -

- -

- -

- +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + +
+
+ + + +
+
+ + + +
+
+
+ + + +
+
-
- - -
-
- - -
-

- Defer SMTP -

+ +
+
+ + +
+
+ + +
+ Defer SMTP +
diff --git a/src/test/java/com/lab/labtimesheet/feature/account/controller/BootstrapOnboardingWebIntegrationTest.java b/src/test/java/com/lab/labtimesheet/feature/account/controller/BootstrapOnboardingWebIntegrationTest.java index 49de963..a37d169 100644 --- a/src/test/java/com/lab/labtimesheet/feature/account/controller/BootstrapOnboardingWebIntegrationTest.java +++ b/src/test/java/com/lab/labtimesheet/feature/account/controller/BootstrapOnboardingWebIntegrationTest.java @@ -71,6 +71,8 @@ class BootstrapOnboardingWebIntegrationTest { .with(user("admin@example.com").roles("ADMIN"))) .andExpect(status().isOk()) .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("Back"))) .andExpect(content().string(org.hamcrest.Matchers.containsString("Configure SMTP"))) @@ -91,6 +93,20 @@ class BootstrapOnboardingWebIntegrationTest { .with(csrf())) .andExpect(status().is3xxRedirection()) .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 diff --git a/src/test/java/com/lab/labtimesheet/feature/integration/controller/SmtpOnboardingWebIntegrationTest.java b/src/test/java/com/lab/labtimesheet/feature/integration/controller/SmtpOnboardingWebIntegrationTest.java index 607b318..7aae0a6 100644 --- a/src/test/java/com/lab/labtimesheet/feature/integration/controller/SmtpOnboardingWebIntegrationTest.java +++ b/src/test/java/com/lab/labtimesheet/feature/integration/controller/SmtpOnboardingWebIntegrationTest.java @@ -102,6 +102,11 @@ class SmtpOnboardingWebIntegrationTest { .andExpect(content().string(org.hamcrest.Matchers.containsString("SMTP is active"))) .andExpect(content().string(org.hamcrest.Matchers.not( 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 @@ -111,16 +116,71 @@ class SmtpOnboardingWebIntegrationTest { .with(csrf()) .param("host", "") .param("port", "70000") - .param("securityMode", "STARTTLS") + .param("securityMode", "") .param("username", "smtp-user") - .param("password", "must-not-be-rendered") + .param("password", "") .param("fromAddress", "not-an-email") - .param("fromName", "Safe sender name")) + .param("fromName", "")) .andExpect(status().isOk()) .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("Port must be between"))) .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.not( org.hamcrest.Matchers.containsString("must-not-be-rendered")))); @@ -130,7 +190,8 @@ class SmtpOnboardingWebIntegrationTest { void restrictedWarningPersistsOnAdminPagesUntilActivationAndMutationsRequireCsrf() throws Exception { mockMvc.perform(get("/admin/accounts/new").with(user("admin@example.com").roles("ADMIN"))) .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") .with(user("admin@example.com").roles("ADMIN")) diff --git a/src/test/java/com/lab/labtimesheet/feature/reporting/controller/DashboardControllerWebTest.java b/src/test/java/com/lab/labtimesheet/feature/reporting/controller/DashboardControllerWebTest.java index bc5176c..39b5329 100644 --- a/src/test/java/com/lab/labtimesheet/feature/reporting/controller/DashboardControllerWebTest.java +++ b/src/test/java/com/lab/labtimesheet/feature/reporting/controller/DashboardControllerWebTest.java @@ -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.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.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; @@ -35,15 +36,31 @@ class DashboardControllerWebTest { void adminRendersAdminDashboardForAuthenticatedIdentity() throws Exception { var dashboard = new DashboardView.Admin(2, 1, 1, 3); given(dashboards.admin("admin@example.test")).willReturn(dashboard); + given(smtpConfiguration.hasActiveConfiguration()).willReturn(false); mvc.perform(get("/dashboard").with(user("admin@example.test").roles("ADMIN"))) .andExpect(status().isOk()) .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"); } + @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 void mentorRendersMentorDashboardForAuthenticatedIdentity() throws Exception { 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"))) .andExpect(status().isOk()) .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"); }