Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bed8b502d4 | ||
|
|
90e412cffa | ||
|
|
207c0bfe89 | ||
|
|
ef08717ce5 | ||
|
|
c8d4e9eecc | ||
|
|
46279f740b | ||
|
|
159e634ffc |
@@ -0,0 +1,91 @@
|
||||
# Test Evidence: form-authenticated global calendar access
|
||||
|
||||
- **Test type:** Web
|
||||
- **Requirement IDs:** `AUTH-002`, `CAL-001`, `SEC-001`, `SEC-013`
|
||||
- **Scenario IDs:** `AC-SEC-005`
|
||||
- **Test class/method:** `com.lab.labtimesheet.feature.attendance.controller.CalendarAuthorizationWebIntegrationTest#formAuthenticatedAdminCanOpenCalendarWhileMentorAndInternAreDenied`
|
||||
- **Implementation commit:** `c8d4e9eecc59c78941769487af30953fb31a83c5`
|
||||
|
||||
## Incident scope
|
||||
|
||||
This record covers only the reported HTTP 403 for a fresh Admin session on
|
||||
`GET /attendance/calendar`. The separately supplied 500 about policy
|
||||
materialization is not a calendar-session or identity-mapping claim. It is
|
||||
cross-referenced to
|
||||
`.superpowers/sdd/access-navigation-icon-intern-picker/task-4-intern-dashboard-report.md`,
|
||||
which independently records valid current PostgreSQL policy/constraint state
|
||||
and no reproduction of that 500.
|
||||
|
||||
## Protected behavior
|
||||
|
||||
The persisted first Admin can open global calendar management after a real CSRF-protected form login. Persisted Mentor and Intern accounts, each authenticated by the same form-login path, receive HTTP 403 for that route.
|
||||
|
||||
## Test method
|
||||
|
||||
The test posts the actual bootstrap form, logs in through Spring Security, and follows the resulting session to `/attendance/calendar`. It configures a test-only SMTP probe solely to activate Mentor and Intern accounts through the public AccountService, then logs in those accounts before asserting denial. Spring Boot applies Flyway to PostgreSQL 18.4 through the shared Testcontainers configuration.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
The bootstrap entity always has immutable `ADMIN` role, so its fresh authenticated session must receive HTTP 200 from the Admin-only calendar route. Immutable `MENTOR` and `INTERN` roles are not permitted by `CAL-001`, so their matching fresh authenticated sessions must receive HTTP 403. No calendar mutation is attempted.
|
||||
|
||||
## RED
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw '-Dtest=CalendarAuthorizationWebIntegrationTest#formAuthenticatedAdminCanOpenCalendarWhileMentorAndInternAreDenied' test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
No valid RED occurred. On exact base 8be1b754e188367b260981718a5d33fc2d4d8a3b,
|
||||
the new incident reproducer passed immediately: Tests run: 1, Failures: 0,
|
||||
Errors: 0, Skipped: 0; BUILD SUCCESS. The production authorization guard was
|
||||
not temporarily weakened merely to manufacture a failing result.
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw '-Dtest=CalendarAuthorizationWebIntegrationTest#formAuthenticatedAdminCanOpenCalendarWhileMentorAndInternAreDenied' test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
No production correction was warranted. The strengthened regression, including
|
||||
form-login authority assertions, passed: Tests run: 1, Failures: 0, Errors: 0,
|
||||
Skipped: 0; BUILD SUCCESS. It observed Admin HTTP 200 and Mentor/Intern HTTP
|
||||
403 after distinct persisted-account logins.
|
||||
```
|
||||
|
||||
## Affected suite
|
||||
|
||||
**Command and result**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest=CalendarAuthorizationWebIntegrationTest,AttendanceControllerTest,AttendanceTemplateIntegrationTest,AuthenticationWebIntegrationTest,SecurityResponseIntegrationTest,RoleDashboardWebIntegrationTest test
|
||||
|
||||
Tests run: 16, Failures: 0, Errors: 0, Skipped: 0; BUILD SUCCESS.
|
||||
|
||||
Full backend suite:
|
||||
./mvnw -q test
|
||||
|
||||
Result: exit code 0 with Java 25.0.4 and PostgreSQL 18.4 Testcontainers.
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
This web test uses real Spring MVC, form authentication, account identity mapping, Flyway, and PostgreSQL 18.4. It substitutes only SMTP transport with an in-memory probe, does not exercise calendar mutations or a real browser, and does not establish production deployment configuration.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Test Evidence: dark icon sprite presentation
|
||||
|
||||
- **Test type:** Web
|
||||
- **Requirement IDs:** `UI-006`, `UI-009`, `UI-010`, `UI-018`
|
||||
- **Scenario IDs:** `AC-UI-003`, `AC-UI-005`
|
||||
- **Test class/method:** `com.lab.labtimesheet.ui.UiContractWebTest#generatedLucideSymbolsRetainCurrentColorStrokePresentation`
|
||||
- **Implementation commit:** `pending`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
Every local Lucide sprite symbol retains the source presentation attributes so icons referenced with `<use>` inherit `currentColor` rather than rendering with the SVG default black fill on dark surfaces.
|
||||
|
||||
## Test method
|
||||
|
||||
The focused web contract reads the generated classpath sprite, scans every emitted `<symbol>`, and checks the five presentation attributes on each symbol. It checks the deployable generated artifact rather than generator source text.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
Lucide 1.27.0 line icons use `fill="none"`, `stroke="currentColor"`, `stroke-width="2"`, `stroke-linecap="round"`, and `stroke-linejoin="round"` on their SVG root. Each selected generated symbol must preserve those values.
|
||||
|
||||
## RED
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ./mvnw '-Dtest=UiContractWebTest#generatedLucideSymbolsRetainCurrentColorStrokePresentation' test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
UiContractWebTest.generatedLucideSymbolsRetainCurrentColorStrokePresentation
|
||||
Missing fill on id="bell" viewBox="0 0 24 24" ==> expected: <true> but was: <false>
|
||||
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
|
||||
BUILD FAILURE
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
env PATH=/opt/homebrew/opt/node@24/bin:$PATH npm ci
|
||||
env PATH=/opt/homebrew/opt/node@24/bin:$PATH npm run build
|
||||
env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ./mvnw '-Dtest=UiContractWebTest#generatedLucideSymbolsRetainCurrentColorStrokePresentation' test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Node v24.19.0 and npm 11.17.0 installed the locked dependencies.
|
||||
Tailwind CSS v4.3.3 rebuilt app.css and build-icons regenerated icons.svg.
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
```
|
||||
|
||||
## Affected suite
|
||||
|
||||
**Command and result**
|
||||
|
||||
```text
|
||||
env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ./mvnw '-Dtest=UiContractWebTest' test
|
||||
|
||||
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
The deterministic asset contract proves the generated sprite carries theme-aware Lucide presentation attributes. It does not replace the taskmaster-owned integrated browser/detector pass for rendered layout and interactive states.
|
||||
@@ -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.
|
||||
@@ -10,10 +10,14 @@ const names = [
|
||||
const output = resolve('src/main/resources/static/assets/icons.svg');
|
||||
const symbols = await Promise.all(names.map(async (name) => {
|
||||
const svg = await readFile(resolve(`node_modules/lucide-static/icons/${name}.svg`), 'utf8');
|
||||
const viewBox = svg.match(/viewBox="([^"]+)"/)?.[1] ?? '0 0 24 24';
|
||||
const root = svg.match(/<svg\b([^>]*)>/)?.[1];
|
||||
const viewBox = root?.match(/viewBox="([^"]+)"/)?.[1] ?? '0 0 24 24';
|
||||
const presentation = ['fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin']
|
||||
.map((attribute) => root?.match(new RegExp(`${attribute}="[^"]+"`))?.[0])
|
||||
.join(' ');
|
||||
const body = svg.match(/<svg[\s\S]*?>([\s\S]*?)<\/svg>/)?.[1];
|
||||
if (!body) throw new Error(`Invalid Lucide SVG: ${name}`);
|
||||
return `<symbol id="${name}" viewBox="${viewBox}">${body.trim()}</symbol>`;
|
||||
return `<symbol id="${name}" viewBox="${viewBox}" ${presentation}>${body.trim()}</symbol>`;
|
||||
}));
|
||||
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none"><symbol id="bell" viewBox="0 0 24 24"><path d="M10.268 21a2 2 0 0 0 3.464 0" />
|
||||
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol><symbol id="calendar-days" viewBox="0 0 24 24"><path d="M8 2v4" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none"><symbol id="bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" />
|
||||
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol><symbol id="calendar-days" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4" />
|
||||
<path d="M16 2v4" />
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" />
|
||||
<path d="M3 10h18" />
|
||||
@@ -8,27 +8,27 @@
|
||||
<path d="M16 14h.01" />
|
||||
<path d="M8 18h.01" />
|
||||
<path d="M12 18h.01" />
|
||||
<path d="M16 18h.01" /></symbol><symbol id="check-circle-2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" />
|
||||
<path d="m9 12 2 2 4-4" /></symbol><symbol id="chevron-left" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6" /></symbol><symbol id="chevron-right" viewBox="0 0 24 24"><path d="m9 18 6-6-6-6" /></symbol><symbol id="circle-user-round" viewBox="0 0 24 24"><path d="M17.925 20.056a6 6 0 0 0-11.851.001" />
|
||||
<path d="M16 18h.01" /></symbol><symbol id="check-circle-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" />
|
||||
<path d="m9 12 2 2 4-4" /></symbol><symbol id="chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6" /></symbol><symbol id="chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></symbol><symbol id="circle-user-round" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.925 20.056a6 6 0 0 0-11.851.001" />
|
||||
<circle cx="12" cy="11" r="4" />
|
||||
<circle cx="12" cy="12" r="10" /></symbol><symbol id="clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" /></symbol><symbol id="folder-kanban" viewBox="0 0 24 24"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z" />
|
||||
<circle cx="12" cy="12" r="10" /></symbol><symbol id="clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" /></symbol><symbol id="folder-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z" />
|
||||
<path d="M8 10v4" />
|
||||
<path d="M12 10v2" />
|
||||
<path d="M16 10v6" /></symbol><symbol id="folder-open" viewBox="0 0 24 24"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" /></symbol><symbol id="inbox" viewBox="0 0 24 24"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
||||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /></symbol><symbol id="layout-dashboard" viewBox="0 0 24 24"><rect width="7" height="9" x="3" y="3" rx="1" />
|
||||
<path d="M16 10v6" /></symbol><symbol id="folder-open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" /></symbol><symbol id="inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
||||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /></symbol><symbol id="layout-dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="5" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="9" x="14" y="12" rx="1" />
|
||||
<rect width="7" height="5" x="3" y="16" rx="1" /></symbol><symbol id="list-check" viewBox="0 0 24 24"><path d="M16 5H3" />
|
||||
<rect width="7" height="5" x="3" y="16" rx="1" /></symbol><symbol id="list-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 5H3" />
|
||||
<path d="M16 12H3" />
|
||||
<path d="M11 19H3" />
|
||||
<path d="m15 18 2 2 4-4" /></symbol><symbol id="log-out" viewBox="0 0 24 24"><path d="m16 17 5-5-5-5" />
|
||||
<path d="m15 18 2 2 4-4" /></symbol><symbol id="log-out" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 17 5-5-5-5" />
|
||||
<path d="M21 12H9" />
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /></symbol><symbol id="monitor" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2" />
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /></symbol><symbol id="monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2" />
|
||||
<line x1="8" x2="16" y1="21" y2="21" />
|
||||
<line x1="12" x2="12" y1="17" y2="21" /></symbol><symbol id="moon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" /></symbol><symbol id="panel-left" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M9 3v18" /></symbol><symbol id="settings" viewBox="0 0 24 24"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
|
||||
<circle cx="12" cy="12" r="3" /></symbol><symbol id="sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" />
|
||||
<line x1="12" x2="12" y1="17" y2="21" /></symbol><symbol id="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" /></symbol><symbol id="panel-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M9 3v18" /></symbol><symbol id="settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
|
||||
<circle cx="12" cy="12" r="3" /></symbol><symbol id="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
@@ -36,10 +36,10 @@
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" /></symbol><symbol id="triangle-alert" viewBox="0 0 24 24"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||
<path d="m19.07 4.93-1.41 1.41" /></symbol><symbol id="triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" /></symbol><symbol id="users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<path d="M12 17h.01" /></symbol><symbol id="users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<path d="M16 3.128a4 4 0 0 1 0 7.744" />
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<circle cx="9" cy="7" r="4" /></symbol><symbol id="x" viewBox="0 0 24 24"><path d="M18 6 6 18" />
|
||||
<circle cx="9" cy="7" r="4" /></symbol><symbol id="x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" /></symbol></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -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>
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package com.lab.labtimesheet.feature.attendance.controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import com.lab.labtimesheet.config.TestcontainersConfiguration;
|
||||
import com.lab.labtimesheet.feature.account.model.GlobalRole;
|
||||
import com.lab.labtimesheet.feature.account.model.dto.CreateAccountCommand;
|
||||
import com.lab.labtimesheet.feature.account.service.AccountService;
|
||||
import com.lab.labtimesheet.feature.integration.model.SecurityMode;
|
||||
import com.lab.labtimesheet.feature.integration.model.dto.SmtpConnection;
|
||||
import com.lab.labtimesheet.feature.integration.model.dto.SmtpDraft;
|
||||
import com.lab.labtimesheet.feature.integration.service.SmtpConfigurationService;
|
||||
import com.lab.labtimesheet.feature.integration.service.SmtpProbe;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@Import({TestcontainersConfiguration.class, CalendarAuthorizationWebIntegrationTest.MailProbeConfiguration.class})
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
class CalendarAuthorizationWebIntegrationTest {
|
||||
private static final String PASSWORD = "correct horse battery staple";
|
||||
private static final String ADMIN_EMAIL = "admin@example.test";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private AccountService accounts;
|
||||
|
||||
@Autowired
|
||||
private SmtpConfigurationService smtp;
|
||||
|
||||
@Autowired
|
||||
private RecordingSmtpProbe mail;
|
||||
|
||||
@Test
|
||||
void formAuthenticatedAdminCanOpenCalendarWhileMentorAndInternAreDenied() throws Exception {
|
||||
bootstrapFirstAdminThroughTheForm();
|
||||
MockHttpSession adminSession = login(ADMIN_EMAIL, PASSWORD, "ADMIN");
|
||||
|
||||
mockMvc.perform(get("/attendance/calendar").session(adminSession))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
long adminId = accounts.requireActiveAdminId(ADMIN_EMAIL);
|
||||
configureSmtp(adminId);
|
||||
createAndActivate(adminId, new CreateAccountCommand(
|
||||
"mentor@example.test", "Mentor", GlobalRole.MENTOR, null, null, null));
|
||||
createAndActivate(adminId, new CreateAccountCommand(
|
||||
"intern@example.test",
|
||||
"Intern",
|
||||
GlobalRole.INTERN,
|
||||
"INT-001",
|
||||
LocalDate.of(2026, 8, 1),
|
||||
LocalDate.of(2026, 12, 31)));
|
||||
|
||||
mockMvc.perform(get("/attendance/calendar").session(login("mentor@example.test", PASSWORD, "MENTOR")))
|
||||
.andExpect(status().isForbidden());
|
||||
mockMvc.perform(get("/attendance/calendar").session(login("intern@example.test", PASSWORD, "INTERN")))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
private void bootstrapFirstAdminThroughTheForm() throws Exception {
|
||||
mockMvc.perform(post("/bootstrap")
|
||||
.with(csrf())
|
||||
.param("email", ADMIN_EMAIL)
|
||||
.param("displayName", "Admin")
|
||||
.param("password", PASSWORD))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/admin/smtp?onboarding"));
|
||||
}
|
||||
|
||||
private MockHttpSession login(String email, String password, String role) throws Exception {
|
||||
var result = mockMvc.perform(post("/login")
|
||||
.with(csrf())
|
||||
.param("username", email)
|
||||
.param("password", password))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(authenticated().withUsername(email))
|
||||
.andExpect(authenticated().withRoles(role))
|
||||
.andReturn();
|
||||
return (MockHttpSession) result.getRequest().getSession(false);
|
||||
}
|
||||
|
||||
private void configureSmtp(long adminId) {
|
||||
long draftId = smtp.saveDraft(adminId, new SmtpDraft(
|
||||
"mailpit", 1025, SecurityMode.NONE, null, null, ADMIN_EMAIL, "Lab Timesheet"));
|
||||
smtp.testDraft(draftId, adminId, ADMIN_EMAIL);
|
||||
smtp.activate(draftId, adminId);
|
||||
mail.clear();
|
||||
}
|
||||
|
||||
private void createAndActivate(long adminId, CreateAccountCommand command) {
|
||||
var creation = accounts.create(command, adminId);
|
||||
assertThat(creation.deliverySucceeded()).isTrue();
|
||||
assertThat(accounts.activate(mail.activationTokenFor(command.email()), PASSWORD)).isTrue();
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class MailProbeConfiguration {
|
||||
@Bean
|
||||
@Primary
|
||||
RecordingSmtpProbe recordingSmtpProbe() {
|
||||
return new RecordingSmtpProbe();
|
||||
}
|
||||
}
|
||||
|
||||
static final class RecordingSmtpProbe implements SmtpProbe {
|
||||
private final List<Message> messages = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void send(SmtpConnection connection, String recipient, String subject, String body) {
|
||||
messages.add(new Message(recipient, body));
|
||||
}
|
||||
|
||||
void clear() {
|
||||
messages.clear();
|
||||
}
|
||||
|
||||
String activationTokenFor(String recipient) {
|
||||
String body = messages.stream()
|
||||
.filter(message -> message.recipient().equals(recipient))
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.body();
|
||||
int tokenStart = body.indexOf("token=");
|
||||
assertThat(tokenStart).isGreaterThanOrEqualTo(0);
|
||||
return body.substring(tokenStart + "token=".length()).trim();
|
||||
}
|
||||
}
|
||||
|
||||
record Message(String recipient, String body) {
|
||||
}
|
||||
}
|
||||
+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());
|
||||
|
||||
@@ -97,6 +97,26 @@ class UiContractWebTest {
|
||||
assertTrue(themeBootstrap.contains("matchMedia('(prefers-color-scheme: dark)')"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedLucideSymbolsRetainCurrentColorStrokePresentation() throws Exception {
|
||||
String icons = new ClassPathResource("static/assets/icons.svg")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Matcher symbols = Pattern.compile("<symbol\\b([^>]*)>").matcher(icons);
|
||||
int symbolCount = 0;
|
||||
|
||||
while (symbols.find()) {
|
||||
String attributes = symbols.group(1);
|
||||
assertTrue(attributes.contains("fill=\"none\""), () -> "Missing fill on " + attributes);
|
||||
assertTrue(attributes.contains("stroke=\"currentColor\""), () -> "Missing stroke on " + attributes);
|
||||
assertTrue(attributes.contains("stroke-width=\"2\""), () -> "Missing stroke width on " + attributes);
|
||||
assertTrue(attributes.contains("stroke-linecap=\"round\""), () -> "Missing stroke linecap on " + attributes);
|
||||
assertTrue(attributes.contains("stroke-linejoin=\"round\""), () -> "Missing stroke linejoin on " + attributes);
|
||||
symbolCount++;
|
||||
}
|
||||
|
||||
assertTrue(symbolCount > 0, "The generated sprite must contain symbols");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin@example.test", roles = "ADMIN")
|
||||
void collapsedSidebarExposesStateAndKeyboardVisibleControlNames() throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user