Skip to content

Feature/synchronize UI design#52

Merged
Martin-E-Karlsson merged 25 commits into
mainfrom
feature/synchronize-ui-design
Apr 27, 2026
Merged

Feature/synchronize UI design#52
Martin-E-Karlsson merged 25 commits into
mainfrom
feature/synchronize-ui-design

Conversation

@Martin-E-Karlsson

@Martin-E-Karlsson Martin-E-Karlsson commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • SYSADMIN-only Visa and User Audit Log pages with filtering, date ranges and pagination.
    • Role-specific landing pages: My Applications (users) and Visa Cases (admins/sysadmins).
  • UI Improvements

    • Centralized design tokens and shared component stylesheet; many pages restyled for consistency.
    • Error and profile pages made authentication-aware; forms and details pages updated to use shared styles.
  • Navigation Updates

    • Root/post-login now redirect to /home; header links updated and logout changed to CSRF-protected POST.
  • Tests

    • Added/updated web and unit tests covering new log pages, routing and case-query logic.

- Added header to all templates where relevant
- Removed dashboard/applicant and redirected directly to visas/dashboard
- Added sign out logic to UserViewController and SecurityConfig
- More links to dashboard/applicant redirected directly to visas/dashboard
- Removed dashboards and replaced them with function specific views which is handled by the new /home endpoint
- Separated visas/dashboard into /visa/cases for admins and visa/my-application for users
- Added new log/visa and log/user templates which shows respective log information with filtering and pagination.
- Made all the new views accessible based on authorization in the header

Fix:
- GlobalExceptionHandler now correctly redirects to home instead of dashboard through a button on the template.
- Changed /visas endpoint to /visa and fixed reference in SecurityConfig
- Made /log only accessible to sysadmins in the SecurityConfig
Fix: Removed tests related to /dashboard
…serLogService

Fix: Changed Reference to Visa ID on templates
@Martin-E-Karlsson Martin-E-Karlsson linked an issue Apr 24, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Root now redirects to /home with role-based targets; new paginated/filterable audit log controller/views added; visa routes renamed from /visas to /visa with case-list endpoints and service/repo support; security and logout behavior adjusted; many templates refactored to shared CSS; tests and build config updated.

Changes

Cohort / File(s) Summary
Application & Security
src/main/java/.../ApplicationViewController.java, src/main/java/.../config/SecurityConfig.java
Root now redirects to /home; /dashboard/home; form-login default success → /home; logout moved to POST /user/logout; /log/** restricted to SYSADMIN; visa base path changed to /visa/**.
Audit logs (new)
src/main/java/.../audit/controller/LogViewController.java, src/main/java/.../audit/service/..., src/main/java/.../audit/repository/..., src/test/.../audit/*
Adds LogViewController (/log/visa, /log/user) and findFiltered(...) in log services using JPA Specifications and pageable; repositories now extend JpaSpecificationExecutor; comprehensive controller/service tests added.
Visa controllers/services/repos & templates
src/main/java/.../visa/controller/VisaViewController.java, src/main/java/.../visa/service/VisaService.java, src/main/java/.../visa/repository/VisaRepository.java, src/main/resources/templates/visa/*, src/test/.../visa/*
Controller base path /visas/visa; adds /visa/my-applications and /visa/cases; service methods for open/unassigned/handled cases added; repository query methods added; templates renamed/refactored and tests updated.
User controller & auth flows
src/main/java/.../user/controller/UserViewController.java, src/test/.../user/controller/UserViewControllerTest.java
UserViewController simplified to only UserService; signup/login GET accept principal and redirect authenticated users to /home; added POST /user/logout to clear security context; dashboard endpoints removed; tests adjusted.
Exception handling
src/main/java/.../exception/GlobalExceptionHandler.java
@ControllerAdvice now includes LogViewController; error handlers simplified and commented dashboard URL logic removed.
Templates & fragments
src/main/resources/templates/..., src/main/resources/templates/fragments/cases.html
Removed legacy dashboard templates; added log/visa.html, log/user.html, visa/cases.html, visa/my-applications.html; added fragments/cases; header fragment updated to POST logout and new role links; many templates refactored to shared CSS and /visa routes.
Design tokens & components (CSS)
src/main/resources/static/css/tokens.css, src/main/resources/static/css/components.css
Introduced tokens.css and components.css with design tokens and shared UI components; replaced large amounts of inline styling across templates.
Build & tests
pom.xml, various src/test/...
Added Hibernate annotation processor to compiler annotationProcessorPaths; extensive unit/web tests added/rewritten for audit, visa, and user controller/service behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant LogCtrl as LogViewController
    participant Service as VisaLogService
    participant Repo as VisaLogRepository
    participant DB as Database

    Client->>LogCtrl: GET /log/visa?eventType=&from=&to=&page=&size=
    LogCtrl->>LogCtrl: normalize dates, clamp page/size, build PageRequest
    LogCtrl->>Service: findFiltered(eventType, fromDateTime, toDateTime, pageable)
    Service->>Service: build JPA Specification (predicates)
    Service->>Repo: findAll(spec, pageable)
    Repo->>DB: execute query with predicates + pagination
    DB-->>Repo: Page<VisaLog>
    Repo-->>Service: Page<VisaLog>
    Service->>Service: map entities -> DTOs
    Service-->>LogCtrl: Page<VisaLogDTO>
    LogCtrl->>Client: render view `log/visa` (model: logs, eventTypes, filters)
Loading
sequenceDiagram
    participant Client as Client
    participant AppCtrl as ApplicationViewController
    participant Sec as SecurityContext
    participant VisaCtrl as VisaViewController

    Client->>AppCtrl: GET /
    AppCtrl->>AppCtrl: redirect / -> /home
    Client->>AppCtrl: GET /home
    AppCtrl->>Sec: iterate authorities -> set flags
    alt SYSADMIN
        Sec-->>AppCtrl: ROLE_SYSADMIN
        AppCtrl-->>Client: redirect /log/visa
    else ADMIN
        Sec-->>AppCtrl: ROLE_ADMIN
        AppCtrl-->>Client: redirect /visa/cases
    else
        AppCtrl-->>Client: redirect /visa/my-applications
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • eeebbaandersson
  • EdvinSandgren

Poem

🐇 I hopped through routes both new and old,

/home now hums where dashboards told,
Logs page by page, with timestamps bright,
Tokens and components dress each sight,
A rabbit cheers: the UI’s right!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Feature/synchronize UI design' is overly broad and vague. It does not clearly communicate the main objectives, which include removing applicantId from CreateVisaDTO, adding CSRF tokens, updating routing endpoints, and implementing new logging features. Consider using a more specific title that highlights the primary change, such as 'Refactor UI routing and add audit logging views' or 'Update routing endpoints and implement CSRF security with audit logs'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/synchronize-ui-design

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (17)
src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java (2)

41-41: Restrict /user/logout matcher to POST to match the controller.

UserViewController.logout is a @PostMapping("/user/logout"), but the matcher here authorizes the path for any HTTP method. A GET to /user/logout by an authenticated user will pass authorization and fall through to a 404 (or to Spring's default error handling), which is slightly surprising and, more importantly, invites accidental logout links. Constraining this to POST aligns the security rule with the actual handler and makes the intent explicit.

🔒 Proposed fix
-                        .requestMatchers("/user/logout").authenticated()
+                        .requestMatchers(org.springframework.http.HttpMethod.POST, "/user/logout").authenticated()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`
at line 41, The security rule currently allows any HTTP method for
"/user/logout" in SecurityConfig by using
requestMatchers("/user/logout").authenticated(); restrict this to POST to match
the controller by changing the matcher to target POST (e.g., use
requestMatchers(HttpMethod.POST, "/user/logout").authenticated() or the
framework's post-specific matcher) and add the necessary HttpMethod import;
reference SecurityConfig and UserViewController.logout to ensure the rule aligns
with the `@PostMapping` handler.

60-60: Remove httpBasic(withDefaults()) from the security configuration.

HTTP Basic authentication is not used anywhere in the codebase—no tests reference it, no clients depend on it, and the application relies exclusively on form login for authentication. Keeping an unused authentication mechanism expands the attack surface without providing any benefit. If HTTP Basic support is needed in the future, it can be re-added with proper documentation and test coverage at that time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`
at line 60, Remove the HTTP Basic configuration call from SecurityConfig by
deleting the .httpBasic(withDefaults()) invocation inside the HttpSecurity
configuration (the method configuring HttpSecurity in class SecurityConfig);
ensure formLogin() remains configured and that any static import of
withDefaults() is removed, and run/adjust security tests to reflect that HTTP
Basic is no longer enabled.
src/main/resources/static/css/tokens.css (2)

128-129: Minor: --radius-lg and --radius-pill aliasing is fine, but consider a true pill value.

The comment is honest that both resolve to 12px today. For actual pill buttons/badges most design systems use 9999px (or 999em) so the shape is radius-independent of element height. If a pill treatment is ever applied to a tall element, the current 12px will silently look like a rounded rectangle instead of a pill.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/css/tokens.css` around lines 128 - 129, The current
CSS token alias --radius-pill equals --radius-lg (12px), which will fail to
produce true pill shapes for taller elements; change the --radius-pill token to
a height-independent large value (e.g., 9999px or 999em) instead of 12px so pill
buttons/badges always render fully rounded, and update any related comment to
note it's intentionally a max-radius for pill shapes (referencing the
--radius-lg and --radius-pill tokens).

120-121: Stylelint value-keyword-case errors here are false positives — don't lowercase these font names.

Font family identifiers like BlinkMacSystemFont, Roboto, SFMono-Regular, Menlo, and Consolas are proper-case by convention and match what Apple/Google/Microsoft actually ship. Lowercasing them is technically valid CSS (idents are case-insensitive in font-family) but is unusual and hurts readability.

Prefer to silence the rule for font stacks rather than accommodate it:

🔧 Suggested stylelint config tweak
 "value-keyword-case": [
   "lower",
   {
+    "ignoreProperties": ["/font/", "font-family"],
+    "ignoreKeywords": ["BlinkMacSystemFont", "Roboto", "SFMono-Regular", "Menlo", "Consolas"]
   }
 ]

Or add an inline ignore next to the declarations:

+    /* stylelint-disable value-keyword-case -- proper-case font family names */
     --font-system: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
     --font-mono:   ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+    /* stylelint-enable value-keyword-case */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/css/tokens.css` around lines 120 - 121, The
stylelint rule value-keyword-case is flagging proper-case font names as false
positives in the CSS variables --font-system and --font-mono; fix by silencing
the rule for these declarations — either add an inline suppression comment
immediately before/after the --font-system and --font-mono lines (e.g. a
stylelint-disable-next-line/value-keyword-case around those declarations) or
update the stylelint config to ignore value-keyword-case for font-family values
(e.g. allow-list or an overrides pattern for tokens.css) so the font names
remain properly cased.
src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java (2)

38-61: Recommended refactor: deduplicate findFiltered across VisaLogService and UserLogService.

UserLogService#findFiltered (Lines 44-61 of that file) is a near-identical copy of this method — only the entity type and the event-type attribute name differ. If a filter gets added (e.g. actorUserId, visaCaseId) it will have to be applied in two places and kept in sync.

A couple of directions worth considering:

  • Extract a small generic helper buildTimeRangeSpec(String eventAttr, Enum<?> eventValue, LocalDateTime from, LocalDateTime to) returning Specification<T>, and have each service call it plus map.
  • Or introduce a tiny abstract base (AbstractAuditLogService<E, DTO>) that owns findFiltered and delegates the event-attribute name + mapper to subclasses.

Not blocking — flagging because the duplication is likely to bit-rot as the audit pages evolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java`
around lines 38 - 61, The two near-identical methods VisaLogService.findFiltered
and UserLogService.findFiltered should be deduplicated: extract a reusable
Specification builder (e.g. buildTimeRangeSpec(String eventAttr, Enum<?>
eventValue, LocalDateTime from, LocalDateTime to) returning Specification<T>) or
introduce an AbstractAuditLogService<E, DTO> that implements findFiltered and
delegates the entity-specific pieces (the event attribute name and the mapper)
to subclasses; update VisaLogService and UserLogService to call the shared
helper/base method (keep visaLogMapper::toDTO and the repository calls in
subclasses if using the abstract base) so future filters (actorUserId,
visaCaseId, etc.) are added once.

44-61: Attribute names in the Specification are correct; consider adding compile-time safety for long-term maintenance.

The hardcoded strings root.get("visaEventType") and root.get("timeStamp") correctly match the VisaLog entity fields. However, string-based attribute access remains runtime-verified—a future field rename could silently fail until the endpoint is exercised.

For long-term robustness, consider one of these approaches:

  • Use the JPA static metamodel (e.g., VisaLog_.timeStamp, VisaLog_.visaEventType) generated by Hibernate JPA Modelgen, providing compile-time verification.
  • Extract a typed Specification<VisaLog> factory method to centralize the attribute names and reduce duplication with UserLogService.findFiltered.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java`
around lines 44 - 61, The Specification in VisaLogService.findFiltered uses
string attribute names ("visaEventType", "timeStamp") which are runtime-checked;
switch to a compile-time safe approach by either using the JPA static metamodel
(e.g., replace root.get("timeStamp") and root.get("visaEventType") with
VisaLog_.timeStamp and VisaLog_.visaEventType) or extract a typed Specification
factory method (e.g., createVisaLogSpecification(...) used by findFiltered,
similar to UserLogService.findFiltered) that centralizes attribute access so
renames are caught at compile time.
src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java (1)

311-446: Optional: extract a small test helper for VisaDTO construction.

The six new tests each instantiate VisaDTO with 15 positional null arguments (e.g., Lines 324–327, 367–368, 411–414). This is fine today but will force every test to be touched if the DTO gains or reorders fields. A visaDtoWithId(Long id) helper would make intent clearer and localize future breakage.

♻️ Proposed helper
+    private static VisaDTO visaDtoWithId(Long id) {
+        return new VisaDTO(id, null, null, null, null, null,
+                null, null, null, null, null, null, null, null, null);
+    }

Then in each test:

-        VisaDTO d1 = new VisaDTO(1L, null, null, null, null, null,
-                null, null, null, null, null, null, null, null, null);
+        VisaDTO d1 = visaDtoWithId(1L);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java`
around lines 311 - 446, Extract a small test helper in VisaServiceTest named
something like visaDtoWithId(Long id) that returns a VisaDTO instance with the
given id and default/null values for other fields, then replace all inline new
VisaDTO(...15 nulls...) uses (in tests
findOpenCasesByHandler_shouldMapEntitiesToDTOs_andQueryAssignedAndIncomplete,
findUnassignedCases_shouldMapEntitiesToDTOs_andQuerySubmittedWithNullHandler,
findHandledCasesByHandler_shouldMapEntitiesToDTOs_andQueryGrantedAndRejected,
etc.) with calls to visaDtoWithId(id) to centralize construction and make tests
resilient to DTO field changes.
src/main/resources/templates/visa/details.html (1)

181-195: Brittle string comparison for handler assignment state.

The Action Required banner is only rendered when visa.handlerName == 'Unassigned'. This ties a UI-critical condition to a display label rather than a structural field (e.g. visa.handlerId == null or a dedicated boolean on the DTO). If the label ever gets localised, trimmed, or changed to - for symmetry with other placeholders elsewhere in this template, this branch silently stops rendering and incomplete-status applicants lose the Edit call-to-action. Preferable to gate on an id/null check exposed by VisaDTO.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/details.html` around lines 181 - 195, The
template's th:if currently checks the display label visa.handlerName ==
'Unassigned' which is brittle; change the condition to check the structural
handler identifier (e.g., visa.handlerId == null) or a dedicated boolean on the
DTO instead of handlerName so the banner renders regardless of
label/localization; update the th:if expression that references visa.handlerName
to use visa.handlerId (or the boolean flag) and ensure the rest of the block
(the Action Required banner and Edit link) remains unchanged.
src/main/resources/templates/visa/cases.html (2)

29-32: Inconsistent user-display model attribute.

Other visa templates in this PR render the logged-in user as ${currentUser.fullName} (e.g., apply-form.html line 29, edit-form.html line 79). Here it is ${name}, which means the controller has to seed a different attribute just for this page. If the controller already adds currentUser, prefer ${currentUser.fullName} for consistency; otherwise, rename the controller-side model attribute so the admin/sysadmin landing page follows the same convention as the rest of the visa views.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/cases.html` around lines 29 - 32, The
template uses the model attribute ${name} instead of the project's standard
${currentUser.fullName}; update the view in cases.html to render
${currentUser.fullName} (replace ${name}) so it matches other visa templates, or
if the controller currently only supplies "name" instead of "currentUser",
change the controller to add a "currentUser" model attribute (with a fullName
property) and keep cases.html using ${currentUser.fullName} to maintain
consistency with apply-form.html and edit-form.html.

37-188: Three near-identical case tables — candidate for a Thymeleaf fragment.

Open Cases, Unassigned Cases, and Handled Cases render the exact same 8-column table (Reference/Visa ID aside). Any future column change or styling tweak has to be repeated three times and will inevitably drift. Extracting a fragments/cases :: section(title, list, emptyMsg, countSuffix) fragment would collapse ~120 lines into three fragment invocations and keep the page honest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/cases.html` around lines 37 - 188, The
three near-identical tables for "Open Cases", "Unassigned Cases" and "Handled
Cases" should be extracted into a reusable Thymeleaf fragment (e.g.
fragments/cases :: section(title, list, emptyMsg, countSuffix)) to avoid
duplication; create the fragment that accepts a title, the list variable
(iterated as visa), an empty-state message and count suffix, move the table
markup (including the span with class case-count and th:text logic, the
th:each="visa : ${...}", the status span with th:classappend and the date
formatting using `#temporals`) into that fragment, then replace each table block
with a fragment invocation passing the appropriate title, list (openCases /
unassignedCases / handledCases), empty message and suffix.
src/main/resources/templates/log/user.html (1)

8-41: Duplicated page-scoped CSS with log/visa.html.

The entire <style> block here is byte-for-byte identical to the one in log/visa.html, and the comment acknowledges this. Any future tweak has to be made in two places and will drift otherwise. Consider extracting these rules into components.css (e.g., a shared .log-page / .filter-bar / .pager ruleset) or into a Thymeleaf fragment so both log pages pull from a single source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/log/user.html` around lines 8 - 41, Extract the
duplicated page-scoped CSS block (the rules for .container, .log-count,
.filter-bar, .pager and related selectors) out of user.html and visa.html into a
single shared stylesheet or Thymeleaf fragment (e.g., components.css or a
fragment like fragments/log-styles) and replace the inline <style> in both
templates with a single include: either a <link rel="stylesheet"> to the shared
CSS or a Thymeleaf th:insert/th:replace of the fragment; ensure the shared
resource contains the .container, .log-count, .filter-bar, .filter-bar .field,
.filter-bar label, .filter-bar input/select, .filter-bar .actions, .pager and
.pager .page-info rules so both pages use the same source of truth.
src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java (1)

99-101: Optional: @WithMockUser is effectively redundant alongside loginAsSysadmin().

Every test has both @WithMockUser(roles = "SYSADMIN") and a call to loginAsSysadmin(). The manual SecurityContextHolder.getContext().setAuthentication(auth) in loginAsSysadmin() overwrites whatever @WithMockUser installed, so the annotation is dead weight. Since you've already documented (lines 53–60) that the default principal can't be used, consider dropping @WithMockUser to avoid confusion about which principal is actually active during the test.

Also applies to: 124-125, 159-160, 178-179, 195-196, 224-225, 247-248

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java`
around lines 99 - 101, Remove the redundant `@WithMockUser` annotations from the
tests that call loginAsSysadmin(), since loginAsSysadmin() sets the
SecurityContext directly and overrides the annotation; locate test methods such
as visaLog_AsSysadmin_NoFilters_ShouldReturnPageWithDefaults() (and the other
tests at the commented ranges) and delete the `@WithMockUser`(roles = "SYSADMIN")
declarations so the tests rely solely on the loginAsSysadmin() helper for
authentication.
src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java (1)

299-301: Remove the dead OPEN_STATUSES / HANDLED_STATUSES fields and their stale comment.

These constants exist solely to keep the VisaStatus import alive — the controller does not use VisaStatus anywhere else (the back-link logic on lines 300–301 keys off UserAuthorization, not VisaStatus, so the line 299 comment is also inaccurate). Hiding an unused import behind dead static fields is a code smell; if the symbol isn't needed, drop the import.

♻️ Proposed cleanup
-import org.example.visacasemanagementsystem.visa.VisaStatus;
 import org.example.visacasemanagementsystem.visa.VisaType;
-        // Silence unused-field warnings and make the static VisaStatus enum available to the back-link logic
         model.addAttribute("backUrl",
                 user.userAuthorization() == UserAuthorization.USER ? "/visa/my-applications" : "/visa/cases");
-
-    // Kept to prevent "unused import" removal of VisaStatus
-    `@SuppressWarnings`("unused")
-    private static final List<VisaStatus> OPEN_STATUSES = List.of(VisaStatus.ASSIGNED, VisaStatus.INCOMPLETE);
-    `@SuppressWarnings`("unused")
-    private static final List<VisaStatus> HANDLED_STATUSES = List.of(VisaStatus.GRANTED, VisaStatus.REJECTED);
 }

Also applies to: 315-319

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`
around lines 299 - 301, Remove the dead OPEN_STATUSES and HANDLED_STATUSES
static fields and the stale comment that claims they silence unused-field
warnings in VisaViewController; delete those fields and the comment, remove the
unused import of VisaStatus, and then verify no other code in VisaViewController
(e.g., back-link logic using user.userAuthorization()/UserAuthorization)
references VisaStatus so the class still compiles.
src/main/resources/static/css/components.css (2)

139-153: Heads-up: broad element selectors (input, select, textarea, table, tr:hover td).

These bare-tag rules apply everywhere components.css is loaded, which is site-wide. Any future <input> in a different context (e.g. inline search box, toggle, future component library) will inherit the dark-form styling (background, 100 % width, resize: none) and any table will gain hover borders. That's fine for the current design language, but expect to fight specificity with class-scoped overrides as the app grows. Consider scoping these under a class like .form-group input / .data-table if/when that becomes painful.

Also applies to: 317-339

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/css/components.css` around lines 139 - 153, The
global element selectors in components.css (input, select, textarea, table,
tr:hover td) apply site-wide and will unintentionally style any future
inputs/tables; to fix, narrow the selectors by scoping them under a
namespace/class (for example replace bare selectors with .form-group input,
.form-group select, .form-group textarea for form controls and .data-table
table, .data-table tr:hover td for tables) and move the existing rules (width,
background, resize, hover borders, etc.) into those scoped selectors so global
tags are no longer overridden.

187-187: Use the standard ::file-selector-button pseudo-element instead of WebKit-specific ::-webkit-file-upload-button.

The ::-webkit-file-upload-button pseudo-element is WebKit/Blink-specific and not supported in Firefox or other non-Chromium browsers. Firefox has supported the standard ::file-selector-button pseudo-element since version 82, which works across all modern browsers. Replace the WebKit prefix with the standard pseudo-element for cross-browser consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/css/components.css` at line 187, The CSS rule uses
the WebKit-specific pseudo-element
input[type="file"]::-webkit-file-upload-button which breaks cross-browser
support; replace that selector with the standard
input[type="file"]::file-selector-button and keep the same declarations so the
file input button styling works in Firefox and other non‑Chromium browsers
(update the selector occurrence for
input[type="file"]::-webkit-file-upload-button to
input[type="file"]::file-selector-button).
src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java (1)

517-522: Smoke test is narrower than the comment suggests.

@WebMvcTest(VisaViewController.class) only wires the single controller under test, so this 404 simply confirms VisaViewController itself doesn't map /visas/dashboard — it doesn't prove the old path is absent from the whole app (another @Controller could still pick it up and this test wouldn't catch it). For the "silently coming back during future refactors" guarantee called out in the comment (lines 513–515), a full-context integration test (or at least @SpringBootTest) would be needed. Fine to keep this as-is for cheap coverage, but consider tightening the comment so future readers don't over-trust it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`
around lines 517 - 522, The test oldPluralPath_ShouldNoLongerResolve in
VisaViewControllerTest only verifies VisaViewController (because of
`@WebMvcTest`(VisaViewController.class)), so it cannot guarantee the old
/visas/dashboard path is absent application-wide; either change the test to a
full-context integration test by using `@SpringBootTest` with
`@AutoConfigureMockMvc` (and reuse MockMvc) to assert a 404 across the entire
application, or simply update the test comment to state it only checks
VisaViewController scope (not the whole app) to avoid overclaiming. Ensure
updates reference VisaViewControllerTest, the
oldPluralPath_ShouldNoLongerResolve test method, and the
`@WebMvcTest`(VisaViewController.class) annotation so reviewers can locate and
apply the change.
src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java (1)

44-94: Optional: DRY the two log handlers.

visaLog and userLog are structurally identical aside from the enum type, service, and view name. Not a blocker, but a small generic helper would make future columns/filters easier to add in one place.

♻️ Sketch of a shared helper
private <E extends Enum<E>, D> String renderLogPage(
        E eventType, LocalDate from, LocalDate to, int page, int size,
        BiFunction<E, Pageable, Page<D>> fetchFn,
        E[] allEventTypes, String pageTitle, String view, Model model) {

    Page<D> logs = fetchFn.apply(eventType, buildPageable(page, size));
    model.addAttribute("logs", logs);
    model.addAttribute("eventTypes", allEventTypes);
    model.addAttribute("selectedEventType", eventType);
    model.addAttribute("from", from);
    model.addAttribute("to", to);
    model.addAttribute("pageTitle", pageTitle);
    return view;
}

(Signature above simplified — you'd need a functional interface that also takes from/to.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java`
around lines 44 - 94, visaLog and userLog are duplicate handlers differing only
by event enum, DTO/service and view; extract a generic helper (e.g., private <E
extends Enum<E>, D> String renderLogPage(...)) and have visaLog and userLog
delegate to it. The helper should accept parameters for eventType, from, to,
page, size, a fetch function that takes (eventType, from, to, pageable) and
returns Page<D> (or a small functional interface if three args are needed), the
array of all event types, pageTitle and view name, and the Model; replace the
duplicated attribute-setting logic in visaLog and userLog with calls to
renderLogPage while keeping existing method signatures and using
visaLogService.findFiltered and userLogService.findFiltered as the fetch
functions and VisaEventType.values()/UserEventType.values() and
"log/visa"/"log/user" for views.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/resources/static/css/components.css`:
- Around line 180-198: The file input currently hides the filename by setting
input[type="file"] { color: transparent; width: 105px !important; overflow:
hidden; } and using input[type="file"]::before as a fake button; fix by either
(A) restoring visibility: remove or toggle color: transparent, increase width
(remove the 105px !important) and allow overflow so the native filename can
show, keeping the ::before styled button as an adornment, or (B) switch to the
accessible pattern: keep a visually-hidden input[type="file"] and use a styled
<label for="..."> (replace the present input[type="file"]::before behavior) plus
an adjacent filename <span> that you update in a small change handler on the
input's change event to set the selected filename; update selectors referenced
above (input[type="file"], input[type="file"]::before) accordingly.

In `@src/main/resources/templates/error/error.html`:
- Line 80: The error page copy in templates/error/error.html currently reads
"Please verify your request to home page." — replace that sentence with clearer
wording such as "Please verify your request or return to the home page." Update
the <p> element text in the error template so the message reads the suggested
copy (or an equivalent clearer variant) to avoid the awkward phrasing.

In `@src/main/resources/templates/log/user.html`:
- Around line 107-111: The status badge can NPE when log.userEventType is null;
update the span rendering (the <span class="status" ...> that uses
`#strings.toLowerCase`(log.userEventType.name()) and
th:text="${log.userEventType}") to guard against null by checking
log.userEventType before calling name()/toLowerCase and providing a safe
fallback class/text (e.g., "unknown" or empty) so both th:classappend and
th:text never call name() on a null userEventType.
- Around line 121-140: The pager links currently inject
eventType=${selectedEventType} without null-checking, which emits eventType=
when no filter is set; update both pager anchor hrefs that reference
selectedEventType (the "Previous" and "Next" links within the pager block
referencing logs.number, logs.size, selectedEventType, from, to) to use the same
ternary pattern used for from/to (e.g. eventType=${selectedEventType != null ?
selectedEventType : null}) so the parameter is omitted when unset; apply the
same change to the corresponding pager links in log/visa.html as well.

In `@src/main/resources/templates/log/visa.html`:
- Around line 110-114: Null checks are missing for log.visaEventType and pager
eventType param: update the span to avoid calling name() on null (e.g., use a
ternary like log.visaEventType != null ? 'status-' +
`#strings.toLowerCase`(log.visaEventType.name()) : '' for th:classappend and
log.visaEventType != null ? log.visaEventType : '' for th:text) and change the
pager link parameters to mirror the from/to null-ternary pattern used elsewhere
so eventType is emitted as selectedEventType != null ? selectedEventType : ''
(instead of emitting an empty "eventType=" which breaks enum binding).

In `@src/main/resources/templates/visa/my-applications.html`:
- Line 25: The template uses property-style access on Java record DTOs; update
occurrences of ${currentUser.fullName} and the visa property usages to
method-style calls to match existing templates: replace ${currentUser.fullName}
with ${currentUser.fullName()}, and for each visa entry replace ${visa.id},
${visa.visaType}, ${visa.visaStatus}, ${visa.travelDate}, ${visa.updatedAt} with
${visa.id()}, ${visa.visaType()}, ${visa.visaStatus()}, ${visa.travelDate()},
${visa.updatedAt()} respectively so the template uses the record accessor
methods (currentUser.fullName(), visa.id(), visa.visaType(), visa.visaStatus(),
visa.travelDate(), visa.updatedAt()).

---

Nitpick comments:
In
`@src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java`:
- Around line 44-94: visaLog and userLog are duplicate handlers differing only
by event enum, DTO/service and view; extract a generic helper (e.g., private <E
extends Enum<E>, D> String renderLogPage(...)) and have visaLog and userLog
delegate to it. The helper should accept parameters for eventType, from, to,
page, size, a fetch function that takes (eventType, from, to, pageable) and
returns Page<D> (or a small functional interface if three args are needed), the
array of all event types, pageTitle and view name, and the Model; replace the
duplicated attribute-setting logic in visaLog and userLog with calls to
renderLogPage while keeping existing method signatures and using
visaLogService.findFiltered and userLogService.findFiltered as the fetch
functions and VisaEventType.values()/UserEventType.values() and
"log/visa"/"log/user" for views.

In
`@src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java`:
- Around line 38-61: The two near-identical methods VisaLogService.findFiltered
and UserLogService.findFiltered should be deduplicated: extract a reusable
Specification builder (e.g. buildTimeRangeSpec(String eventAttr, Enum<?>
eventValue, LocalDateTime from, LocalDateTime to) returning Specification<T>) or
introduce an AbstractAuditLogService<E, DTO> that implements findFiltered and
delegates the entity-specific pieces (the event attribute name and the mapper)
to subclasses; update VisaLogService and UserLogService to call the shared
helper/base method (keep visaLogMapper::toDTO and the repository calls in
subclasses if using the abstract base) so future filters (actorUserId,
visaCaseId, etc.) are added once.
- Around line 44-61: The Specification in VisaLogService.findFiltered uses
string attribute names ("visaEventType", "timeStamp") which are runtime-checked;
switch to a compile-time safe approach by either using the JPA static metamodel
(e.g., replace root.get("timeStamp") and root.get("visaEventType") with
VisaLog_.timeStamp and VisaLog_.visaEventType) or extract a typed Specification
factory method (e.g., createVisaLogSpecification(...) used by findFiltered,
similar to UserLogService.findFiltered) that centralizes attribute access so
renames are caught at compile time.

In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`:
- Line 41: The security rule currently allows any HTTP method for "/user/logout"
in SecurityConfig by using requestMatchers("/user/logout").authenticated();
restrict this to POST to match the controller by changing the matcher to target
POST (e.g., use requestMatchers(HttpMethod.POST, "/user/logout").authenticated()
or the framework's post-specific matcher) and add the necessary HttpMethod
import; reference SecurityConfig and UserViewController.logout to ensure the
rule aligns with the `@PostMapping` handler.
- Line 60: Remove the HTTP Basic configuration call from SecurityConfig by
deleting the .httpBasic(withDefaults()) invocation inside the HttpSecurity
configuration (the method configuring HttpSecurity in class SecurityConfig);
ensure formLogin() remains configured and that any static import of
withDefaults() is removed, and run/adjust security tests to reflect that HTTP
Basic is no longer enabled.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`:
- Around line 299-301: Remove the dead OPEN_STATUSES and HANDLED_STATUSES static
fields and the stale comment that claims they silence unused-field warnings in
VisaViewController; delete those fields and the comment, remove the unused
import of VisaStatus, and then verify no other code in VisaViewController (e.g.,
back-link logic using user.userAuthorization()/UserAuthorization) references
VisaStatus so the class still compiles.

In `@src/main/resources/static/css/components.css`:
- Around line 139-153: The global element selectors in components.css (input,
select, textarea, table, tr:hover td) apply site-wide and will unintentionally
style any future inputs/tables; to fix, narrow the selectors by scoping them
under a namespace/class (for example replace bare selectors with .form-group
input, .form-group select, .form-group textarea for form controls and
.data-table table, .data-table tr:hover td for tables) and move the existing
rules (width, background, resize, hover borders, etc.) into those scoped
selectors so global tags are no longer overridden.
- Line 187: The CSS rule uses the WebKit-specific pseudo-element
input[type="file"]::-webkit-file-upload-button which breaks cross-browser
support; replace that selector with the standard
input[type="file"]::file-selector-button and keep the same declarations so the
file input button styling works in Firefox and other non‑Chromium browsers
(update the selector occurrence for
input[type="file"]::-webkit-file-upload-button to
input[type="file"]::file-selector-button).

In `@src/main/resources/static/css/tokens.css`:
- Around line 128-129: The current CSS token alias --radius-pill equals
--radius-lg (12px), which will fail to produce true pill shapes for taller
elements; change the --radius-pill token to a height-independent large value
(e.g., 9999px or 999em) instead of 12px so pill buttons/badges always render
fully rounded, and update any related comment to note it's intentionally a
max-radius for pill shapes (referencing the --radius-lg and --radius-pill
tokens).
- Around line 120-121: The stylelint rule value-keyword-case is flagging
proper-case font names as false positives in the CSS variables --font-system and
--font-mono; fix by silencing the rule for these declarations — either add an
inline suppression comment immediately before/after the --font-system and
--font-mono lines (e.g. a stylelint-disable-next-line/value-keyword-case around
those declarations) or update the stylelint config to ignore value-keyword-case
for font-family values (e.g. allow-list or an overrides pattern for tokens.css)
so the font names remain properly cased.

In `@src/main/resources/templates/log/user.html`:
- Around line 8-41: Extract the duplicated page-scoped CSS block (the rules for
.container, .log-count, .filter-bar, .pager and related selectors) out of
user.html and visa.html into a single shared stylesheet or Thymeleaf fragment
(e.g., components.css or a fragment like fragments/log-styles) and replace the
inline <style> in both templates with a single include: either a <link
rel="stylesheet"> to the shared CSS or a Thymeleaf th:insert/th:replace of the
fragment; ensure the shared resource contains the .container, .log-count,
.filter-bar, .filter-bar .field, .filter-bar label, .filter-bar input/select,
.filter-bar .actions, .pager and .pager .page-info rules so both pages use the
same source of truth.

In `@src/main/resources/templates/visa/cases.html`:
- Around line 29-32: The template uses the model attribute ${name} instead of
the project's standard ${currentUser.fullName}; update the view in cases.html to
render ${currentUser.fullName} (replace ${name}) so it matches other visa
templates, or if the controller currently only supplies "name" instead of
"currentUser", change the controller to add a "currentUser" model attribute
(with a fullName property) and keep cases.html using ${currentUser.fullName} to
maintain consistency with apply-form.html and edit-form.html.
- Around line 37-188: The three near-identical tables for "Open Cases",
"Unassigned Cases" and "Handled Cases" should be extracted into a reusable
Thymeleaf fragment (e.g. fragments/cases :: section(title, list, emptyMsg,
countSuffix)) to avoid duplication; create the fragment that accepts a title,
the list variable (iterated as visa), an empty-state message and count suffix,
move the table markup (including the span with class case-count and th:text
logic, the th:each="visa : ${...}", the status span with th:classappend and the
date formatting using `#temporals`) into that fragment, then replace each table
block with a fragment invocation passing the appropriate title, list (openCases
/ unassignedCases / handledCases), empty message and suffix.

In `@src/main/resources/templates/visa/details.html`:
- Around line 181-195: The template's th:if currently checks the display label
visa.handlerName == 'Unassigned' which is brittle; change the condition to check
the structural handler identifier (e.g., visa.handlerId == null) or a dedicated
boolean on the DTO instead of handlerName so the banner renders regardless of
label/localization; update the th:if expression that references visa.handlerName
to use visa.handlerId (or the boolean flag) and ensure the rest of the block
(the Action Required banner and Edit link) remains unchanged.

In
`@src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java`:
- Around line 99-101: Remove the redundant `@WithMockUser` annotations from the
tests that call loginAsSysadmin(), since loginAsSysadmin() sets the
SecurityContext directly and overrides the annotation; locate test methods such
as visaLog_AsSysadmin_NoFilters_ShouldReturnPageWithDefaults() (and the other
tests at the commented ranges) and delete the `@WithMockUser`(roles = "SYSADMIN")
declarations so the tests rely solely on the loginAsSysadmin() helper for
authentication.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java`:
- Around line 311-446: Extract a small test helper in VisaServiceTest named
something like visaDtoWithId(Long id) that returns a VisaDTO instance with the
given id and default/null values for other fields, then replace all inline new
VisaDTO(...15 nulls...) uses (in tests
findOpenCasesByHandler_shouldMapEntitiesToDTOs_andQueryAssignedAndIncomplete,
findUnassignedCases_shouldMapEntitiesToDTOs_andQuerySubmittedWithNullHandler,
findHandledCasesByHandler_shouldMapEntitiesToDTOs_andQueryGrantedAndRejected,
etc.) with calls to visaDtoWithId(id) to centralize construction and make tests
resilient to DTO field changes.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`:
- Around line 517-522: The test oldPluralPath_ShouldNoLongerResolve in
VisaViewControllerTest only verifies VisaViewController (because of
`@WebMvcTest`(VisaViewController.class)), so it cannot guarantee the old
/visas/dashboard path is absent application-wide; either change the test to a
full-context integration test by using `@SpringBootTest` with
`@AutoConfigureMockMvc` (and reuse MockMvc) to assert a 404 across the entire
application, or simply update the test comment to state it only checks
VisaViewController scope (not the whole app) to avoid overclaiming. Ensure
updates reference VisaViewControllerTest, the
oldPluralPath_ShouldNoLongerResolve test method, and the
`@WebMvcTest`(VisaViewController.class) annotation so reviewers can locate and
apply the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8d8cf9c-d50b-4c1d-bc26-b95e36146562

📥 Commits

Reviewing files that changed from the base of the PR and between 325552a and 2d174c8.

📒 Files selected for processing (38)
  • src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java
  • src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java
  • src/main/java/org/example/visacasemanagementsystem/audit/repository/UserLogRepository.java
  • src/main/java/org/example/visacasemanagementsystem/audit/repository/VisaLogRepository.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java
  • src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java
  • src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java
  • src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java
  • src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java
  • src/main/resources/static/css/components.css
  • src/main/resources/static/css/tokens.css
  • src/main/resources/templates/dashboard/admin.html
  • src/main/resources/templates/dashboard/applicant.html
  • src/main/resources/templates/dashboard/sysadmin.html
  • src/main/resources/templates/error/error.html
  • src/main/resources/templates/fragments/header.html
  • src/main/resources/templates/log/user.html
  • src/main/resources/templates/log/visa.html
  • src/main/resources/templates/profile/edit.html
  • src/main/resources/templates/profile/view.html
  • src/main/resources/templates/user/list.html
  • src/main/resources/templates/user/login.html
  • src/main/resources/templates/user/signup.html
  • src/main/resources/templates/visa/apply-form.html
  • src/main/resources/templates/visa/cases.html
  • src/main/resources/templates/visa/dashboard.html
  • src/main/resources/templates/visa/details.html
  • src/main/resources/templates/visa/edit-form.html
  • src/main/resources/templates/visa/my-applications.html
  • src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java
  • src/test/java/org/example/visacasemanagementsystem/audit/service/UserLogServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/audit/service/VisaLogServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java
💤 Files with no reviewable changes (4)
  • src/main/resources/templates/dashboard/admin.html
  • src/main/resources/templates/visa/dashboard.html
  • src/main/resources/templates/dashboard/sysadmin.html
  • src/main/resources/templates/dashboard/applicant.html

Comment thread src/main/resources/static/css/components.css
Comment thread src/main/resources/templates/error/error.html Outdated
Comment thread src/main/resources/templates/log/user.html
Comment thread src/main/resources/templates/log/visa.html
Comment thread src/main/resources/templates/visa/my-applications.html Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/resources/templates/visa/details.html (1)

107-112: ⚠️ Potential issue | 🟡 Minor

Incomplete handler-name → handler-id migration — "Waiting for assignment" branch is unreachable.

Line 110 still uses visa.handlerName != 'Unassigned', but elsewhere in this PR the sentinel for "no handler" is handlerId == null (Line 181) and handlerName == null (fragment cases.html Line 46). Since handlerName is null (not the literal string "Unassigned") for unassigned cases, the ternary is always true and always renders the (null/blank) visa.handlerName; the "Waiting for assignment" fallback never fires.

🐛 Proposed fix
-        <p class="user-info" th:if="${currentUser.userAuthorization.name() != 'USER'}">
-            Handled by: <span th:text="${visa.handlerName != 'Unassigned' ? visa.handlerName : 'Waiting for assignment'}"></span>
-        </p>
+        <p class="user-info" th:if="${currentUser.userAuthorization.name() != 'USER'}">
+            Handled by: <span th:text="${visa.handlerId != null ? visa.handlerName : 'Waiting for assignment'}"></span>
+        </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/details.html` around lines 107 - 112, The
template's ternary uses the literal string check visa.handlerName !=
'Unassigned' which no longer matches the migration where unassigned cases use
null; update the condition in the header block so the fallback "Waiting for
assignment" is used when visa.handlerId is null or visa.handlerName is null
(e.g., change the th:if / ternary to check visa.handlerId == null or
visa.handlerName == null and render handlerName otherwise), ensuring the same
sentinel as used elsewhere (handlerId == null / handlerName == null).
🧹 Nitpick comments (1)
src/main/resources/templates/fragments/cases.html (1)

25-29: Prefer #lists.isEmpty(cases) for null-safety and consistency.

cases.isEmpty() will NPE if a caller ever passes null for the cases parameter, and it's inconsistent with other templates in this PR (e.g., details.html uses #lists.isEmpty(visa.downloadUrls)). Today's callers always pass a non-null list from the controller, but the fragment is a reusable abstraction and should defend against future callers.

♻️ Proposed fix
-    <div th:if="${cases.isEmpty()}" class="empty-state" th:text="${emptyMsg}">
+    <div th:if="${`#lists.isEmpty`(cases)}" class="empty-state" th:text="${emptyMsg}">
         No cases.
     </div>

-    <table th:if="${!cases.isEmpty()}">
+    <table th:if="${!#lists.isEmpty(cases)}">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/cases.html` around lines 25 - 29,
Replace the direct calls to cases.isEmpty() with Thymeleaf's null-safe list
utility: change th:if="${cases.isEmpty()}" to th:if="${`#lists.isEmpty`(cases)}"
and the table condition th:if="${!cases.isEmpty()}" to
th:if="${!#lists.isEmpty(cases)}"; this keeps the fragment null-safe and
consistent with other templates (e.g., usage of
`#lists.isEmpty`(visa.downloadUrls)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java`:
- Around line 5-6: Remove the unused DTO imports in LogViewController: delete
the import statements for
org.example.visacasemanagementsystem.audit.dto.UserLogDTO and
org.example.visacasemanagementsystem.audit.dto.VisaLogDTO from the
LogViewController class so spotless:check passes; verify no references to
UserLogDTO or VisaLogDTO remain in LogViewController before committing.

In `@src/main/resources/templates/error/error.html`:
- Line 74: The header fragment include currently uses th:replace with
sec:authorize on the same <nav> element (fragments/header :: nav), but fragment
processing runs before the security check so anonymous users can see it; fix by
moving the security check to a separate wrapper (e.g., add a surrounding element
with sec:authorize="isAuthenticated()" that contains the th:replace call for
fragments/header :: nav) so Thymeleaf only includes the fragment for
authenticated users.

---

Outside diff comments:
In `@src/main/resources/templates/visa/details.html`:
- Around line 107-112: The template's ternary uses the literal string check
visa.handlerName != 'Unassigned' which no longer matches the migration where
unassigned cases use null; update the condition in the header block so the
fallback "Waiting for assignment" is used when visa.handlerId is null or
visa.handlerName is null (e.g., change the th:if / ternary to check
visa.handlerId == null or visa.handlerName == null and render handlerName
otherwise), ensuring the same sentinel as used elsewhere (handlerId == null /
handlerName == null).

---

Nitpick comments:
In `@src/main/resources/templates/fragments/cases.html`:
- Around line 25-29: Replace the direct calls to cases.isEmpty() with
Thymeleaf's null-safe list utility: change th:if="${cases.isEmpty()}" to
th:if="${`#lists.isEmpty`(cases)}" and the table condition
th:if="${!cases.isEmpty()}" to th:if="${!#lists.isEmpty(cases)}"; this keeps the
fragment null-safe and consistent with other templates (e.g., usage of
`#lists.isEmpty`(visa.downloadUrls)).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81f459cc-dc30-4a87-9de9-28f4251be530

📥 Commits

Reviewing files that changed from the base of the PR and between 2d174c8 and 0273adb.

📒 Files selected for processing (20)
  • pom.xml
  • src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java
  • src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java
  • src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java
  • src/main/resources/static/css/components.css
  • src/main/resources/static/css/tokens.css
  • src/main/resources/templates/error/error.html
  • src/main/resources/templates/fragments/cases.html
  • src/main/resources/templates/log/user.html
  • src/main/resources/templates/log/visa.html
  • src/main/resources/templates/visa/cases.html
  • src/main/resources/templates/visa/details.html
  • src/main/resources/templates/visa/my-applications.html
  • src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java
  • src/test/java/org/example/visacasemanagementsystem/audit/service/UserLogServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/audit/service/VisaLogServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java
✅ Files skipped from review due to trivial changes (6)
  • src/main/resources/templates/visa/cases.html
  • src/main/resources/static/css/components.css
  • src/main/resources/templates/visa/my-applications.html
  • src/main/resources/templates/log/visa.html
  • src/test/java/org/example/visacasemanagementsystem/audit/service/VisaLogServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/main/resources/static/css/tokens.css
  • src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java
  • src/main/resources/templates/log/user.html
  • src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java
  • src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java
  • src/test/java/org/example/visacasemanagementsystem/audit/service/UserLogServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java

Comment thread src/main/resources/templates/error/error.html Outdated
@Martin-E-Karlsson

Copy link
Copy Markdown
Contributor Author

@coderabbitai Can you do check the pull request once more since the rate limit was exceeded last?

@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown

@Martin-E-Karlsson: Sure! I'll kick off a full re-review of PR #52 right away.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (18)
src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java (1)

467-502: Optional: extract status groupings to constants.

openStatuses and handledStatuses are recomputed on every call. Hoisting them to private static final List<VisaStatus> constants (e.g. OPEN_STATUSES, HANDLED_STATUSES) would clarify intent and avoid per-invocation list allocation. Non-blocking.

♻️ Proposed refactor
+    private static final List<VisaStatus> OPEN_STATUSES =
+            List.of(VisaStatus.ASSIGNED, VisaStatus.INCOMPLETE);
+    private static final List<VisaStatus> HANDLED_STATUSES =
+            List.of(VisaStatus.GRANTED, VisaStatus.REJECTED);
+
     public List<VisaDTO> findOpenCasesByHandler(Long handlerId) {
-        List<VisaStatus> openStatuses = List.of(VisaStatus.ASSIGNED, VisaStatus.INCOMPLETE);
         return visaRepository.findByHandler_IdAndVisaStatusIn(
                         handlerId,
-                        openStatuses,
+                        OPEN_STATUSES,
                         Sort.by("updatedAt").descending())
                 .stream()
                 .map(visaMapper::toDTO)
                 .toList();
     }
@@
     public List<VisaDTO> findHandledCasesByHandler(Long handlerId) {
-        List<VisaStatus> handledStatuses = List.of(VisaStatus.GRANTED, VisaStatus.REJECTED);
         return visaRepository.findByHandler_IdAndVisaStatusIn(
                         handlerId,
-                        handledStatuses,
+                        HANDLED_STATUSES,
                         Sort.by("updatedAt").descending())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java`
around lines 467 - 502, Extract the per-call lists into class-level constants to
avoid reallocations: define private static final List<VisaStatus> OPEN_STATUSES
= List.of(VisaStatus.ASSIGNED, VisaStatus.INCOMPLETE) and private static final
List<VisaStatus> HANDLED_STATUSES = List.of(VisaStatus.GRANTED,
VisaStatus.REJECTED) in VisaService, then replace the local openStatuses and
handledStatuses variables in findOpenCasesByHandler(Long) and
findHandledCasesByHandler(Long) to use OPEN_STATUSES and HANDLED_STATUSES
respectively (keep the Sort.by("updatedAt").descending() and mapping via
visaMapper::toDTO unchanged).
src/test/java/org/example/visacasemanagementsystem/audit/service/VisaLogServiceTest.java (1)

207-222: Optional: extract the Criteria-mock setup into a helper.

Tests for eventType-only / from-only / to-only repeat ~6 identical lines mocking Root/CriteriaQuery/CriteriaBuilder/Path/Predicate and a root.get(nullable(SingularAttribute.class)) stub. A small helper (e.g. CriteriaMocks fixture()) would cut duplication and let each test focus on the assertion that differs.

Not blocking — the current shape is readable.

Also applies to: 242-256, 277-291

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/audit/service/VisaLogServiceTest.java`
around lines 207 - 222, Tests in VisaLogServiceTest repeat the same Criteria
mocks (Root, CriteriaQuery, CriteriaBuilder, Path, Predicate) and the
root.get(nullable(SingularAttribute.class)) stub across multiple cases; extract
that repeated setup into a small helper (e.g., a private method like
setupCriteriaMocks or a CriteriaMocks fixture) that creates and stubs Root,
CriteriaQuery, CriteriaBuilder, Path and returns the mocks (or assigns fields)
so each test can call the helper and then only assert the specific behavior of
specCaptor.getValue().toPredicate(root, query, cb) and verifications of cb.equal
/ greaterThanOrEqualTo / lessThanOrEqualTo; update tests at the other locations
(around lines noted) to use the helper to remove the duplicated when(...) and
mock(...) lines while keeping existing assertions and specCaptor usage.
src/main/resources/templates/fragments/header.html (1)

41-43: Add an explicit CSRF hidden input to the logout form for defensive consistency, or clarify why login/signup/apply forms rely on auto-injection.

The logout form (lines 41–43) relies on Thymeleaf's Spring Security dialect to auto-inject the CSRF token on POST, while visa operation forms (details.html lines 208, 222, 230) and profile edit forms (profile/edit.html lines 40–41, 80–81) include explicit <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> tokens. However, authentication forms (login.html, signup.html) and the visa apply form (apply-form.html) also omit explicit CSRF, suggesting auto-injection is working across the codebase. If you prefer defensive explicitness, add the CSRF input to the logout form; if auto-injection is reliable, the current form is fine. Either way, the inconsistency should be documented or standardized across all POST forms.

♻️ Proposed explicit CSRF input (optional)
         <form th:action="@{/user/logout}" method="post" class="logout-form">
+            <input type="hidden"
+                   th:name="${_csrf.parameterName}"
+                   th:value="${_csrf.token}" />
             <button type="submit" class="btn-signout">Sign Out</button>
         </form>

Resolves the deferred /visas/logout learning: Sign Out now delegates to /user/logout via a POST form, matching Spring Security conventions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/header.html` around lines 41 - 43, The
logout POST form (form with th:action="@{/user/logout}" and class "logout-form"
containing button "btn-signout") should include an explicit CSRF hidden input
for consistency with other forms; add an input using
th:name="${_csrf.parameterName}" and th:value="${_csrf.token}" inside that form
(or, if you intentionally rely on Thymeleaf/Spring Security auto-injection, add
a short comment in the template explaining that auto-injection is relied upon)
so all POST forms are standardized.
src/main/resources/templates/visa/edit-form.html (2)

79-79: Inconsistent record-accessor style vs other updated templates in this PR.

This file uses property-style access (${currentUser.fullName}, ${updateVisaDto.visaStatus}, ${updateVisaDto.id}, ${visa.id}, etc.), while sibling templates updated in this PR (e.g., visa/my-applications.html) consistently use method-style access (${currentUser.fullName()}, ${visa.id()}). Both work with Spring 6's SpEL on records, but the mixed convention makes the codebase harder to grep and maintain.

For consistency, either standardize this template on method-style accessors (fullName(), id(), visaStatus(), downloadUrls(), s3Keys()) or revisit my-applications.html to use property-style — but pick one and stick with it across the visa templates touched here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/edit-form.html` at line 79, This template
mixes property-style SpEL with the rest of the PR; update all record field
accesses in edit-form.html to use method-style accessors to match the other
templates (e.g., change occurrences of currentUser.fullName,
updateVisaDto.visaStatus, updateVisaDto.id, visa.id and any downloadUrls/s3Keys
references to currentUser.fullName(), updateVisaDto.visaStatus(),
updateVisaDto.id(), visa.id() and downloadUrls()/s3Keys() respectively) so the
visa templates consistently use the method-style record accessors.

137-141: Inline th:onclick with confirm()+JS DOM lookup is fragile and CSP-unfriendly.

The current pattern submits a hidden form via inline onclick script generated by Thymeleaf. This works but:

  • Inline event handlers fail under a strict CSP (script-src without 'unsafe-inline').
  • The fallback if the user clicks Cancel is implicit (the boolean short-circuit).
  • The form is rendered in a separate sibling block (line 164–170), so the markup relies on element-id wiring across far-apart sections.

A cleaner alternative is to inline the delete <form> directly in the document row and use a single inline submit button with a confirm handler, eliminating the cross-element id coupling entirely. This is optional — current behavior is correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/edit-form.html` around lines 137 - 141, The
inline th:onclick that uses confirm(...) and
document.getElementById('delete-doc-' + ${iter.index}).submit() is
CSP-unfriendly and couples the button to a distant hidden form; instead move the
delete <form> (the one currently rendered as the hidden sibling with id
'delete-doc-<index>') into the same document row as the remove <button> and
replace the inline th:onclick wiring with a plain submit button inside that form
(use <button type="submit">) and, if you still want a JS confirmation, attach a
non-inline event listener (or use a data-confirm attribute that a central script
handles) rather than using th:onclick; update references to
delete-doc-${iter.index} and the button markup in the template accordingly.
src/main/resources/templates/visa/my-applications.html (1)

33-68: Empty state rendered below the table — table header still shows when there are no applications.

When ${visas} is empty, the <table> (with its headers) still renders followed by the "You have no applications yet." message, producing an empty table with column headers. Consider rendering either the table or the empty-state, not both.

♻️ Suggested structure
     <main>
-        <table>
+        <table th:if="${not `#lists.isEmpty`(visas)}">
             <thead>
             ...
             </thead>
             <tbody>
             <tr th:each="visa : ${visas}">
                 ...
             </tr>
             </tbody>
         </table>

         <div th:if="${`#lists.isEmpty`(visas)}" class="empty-state">
             You have no applications yet.
         </div>
     </main>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/my-applications.html` around lines 33 - 68,
The template currently renders the <table> (with headers) unconditionally and
separately renders the empty-state when ${visas} is empty, causing an empty
table to appear; change the structure so the table (the <table> element that
iterates th:each="visa : ${visas}" and the header row) is rendered only when the
list is not empty and the "You have no applications yet." message is rendered
when `#lists.isEmpty`(visas) is true — e.g., wrap the entire table with
th:if="${!#lists.isEmpty(visas)}" (or move the th:if from the empty-state to the
table inversion) and keep the existing empty-state div with
th:if="${`#lists.isEmpty`(visas)}"; ensure the th:each on visa and the view link
href (@{/visa/{id}(id=${visa.id()})}) remain unchanged.
src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java (1)

513-525: createMockUser is a misleadingly named helper — it has a hidden side effect on SecurityContextHolder.

The method's name suggests it just creates a UserDTO, but it also mutates global SecurityContextHolder state via setAuthentication(...). Several tests call it purely for the auth side-effect and discard the return value (e.g., lines 146, 300, 364, 385, 403, 423). This makes the tests harder to reason about and easy to misuse.

Combined with @WithMockUser on every test (which Spring Security's test infrastructure already uses to populate the security context), there are two competing mechanisms setting authentication — the explicit setAuthentication here ends up overriding @WithMockUser. Consider either:

  1. Splitting the helper into createMockUserDTO(...) (pure) and mockAuthenticatedAs(...) (side-effecting), or
  2. Dropping @WithMockUser and using only the explicit principal setup, or
  3. Switching fully to Spring Security test support (e.g., with(user(...)) / @WithUserDetails) and removing the manual SecurityContextHolder writes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`
around lines 513 - 525, createMockUser currently both returns a UserDTO and
mutates global SecurityContextHolder via
SecurityContextHolder.getContext().setAuthentication(...), which is misleading
and conflicts with `@WithMockUser`; split it into a pure factory (e.g.,
createMockUserDTO(Long id, UserAuthorization role) that only constructs and
returns UserDTO/User objects) and a side-effecting helper (e.g.,
mockAuthenticatedAs(User user) or mockAuthenticatedAs(UserDTO dto) that creates
a UserPrincipal and TestingAuthenticationToken and calls
SecurityContextHolder.getContext().setAuthentication(...)); update tests that
only need the DTO to call the pure factory and tests that need to set
authentication to call the new mockAuthenticatedAs helper (or remove the manual
SecurityContextHolder writes and rely on `@WithMockUser` / with(user(...))
consistently).
src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java (1)

45-62: LGTM — clean dynamic Specification with proper null-filter handling.

The use of Specification.unrestricted() as the starting point and the conditional and(...) chaining cleanly avoids binding null enum parameters in JPQL, and the mapping via Page#map preserves pagination metadata.

Note: findFiltered here mirrors VisaLogService#findFiltered very closely (same shape, same predicate construction). If a third audit-log type ever lands, consider extracting the predicate-builder pattern (e.g., a small helper that takes a metamodel attribute + value and returns an and lambda) into a shared utility, but this is purely an optional cleanup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java`
around lines 45 - 62, No functional change required—findFiltered in
UserLogService is correct; optionally extract the repeated
predicate-construction used by UserLogService#findFiltered and
VisaLogService#findFiltered into a small shared helper (e.g.,
SpecificationUtils.buildEqualsOrRange) that accepts a metamodel attribute
(UserLog_.userEventType/UserLog_.timeStamp) and a value or range bounds and
returns a Specification to be and()-chained; implement the helper to produce the
same lambdas (equal, greaterThanOrEqualTo, lessThanOrEqualTo) so both services
can call the helper instead of duplicating the spec construction.
src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java (1)

27-40: LGTM — role-based routing aligns with SecurityConfig and downstream @PreAuthorize guards.

The three redirect targets (/log/visa, /visa/cases, /visa/my-applications) all have matching role protections (per SecurityConfig /log/** → SYSADMIN and @PreAuthorize on the visa controller methods), so the routing is consistent.

Optional micro-tweak: the authorities stream is iterated twice. A single pass would be slightly more efficient and read more uniformly:

♻️ Optional simplification
-        boolean isSysAdmin = principal.getAuthorities().stream()
-                .anyMatch(a -> Objects.equals(a.getAuthority(), "ROLE_SYSADMIN"));
-        boolean isAdmin = principal.getAuthorities().stream()
-                .anyMatch(a -> Objects.equals(a.getAuthority(), "ROLE_ADMIN"));
-
-        if (isSysAdmin) return "redirect:/log/visa";
-        if (isAdmin)    return "redirect:/visa/cases";
-        return "redirect:/visa/my-applications";
+        for (var authority : principal.getAuthorities()) {
+            String role = authority.getAuthority();
+            if ("ROLE_SYSADMIN".equals(role)) return "redirect:/log/visa";
+            if ("ROLE_ADMIN".equals(role))    return "redirect:/visa/cases";
+        }
+        return "redirect:/visa/my-applications";

Note: the original code's behavior of preferring SYSADMIN over ADMIN when both are present is preserved only if the iteration finds SYSADMIN first. If a principal can hold both roles and ordering matters, keep the explicit two-pass version.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java`
around lines 27 - 40, The home method currently iterates
principal.getAuthorities() twice to compute isSysAdmin and isAdmin; collapse
this into a single pass over principal.getAuthorities() (inside home) that sets
two booleans (isSysAdmin, isAdmin) as it sees each authority so you avoid double
iteration while preserving the existing preference logic (ensure SYSADMIN
redirect still takes precedence if both flags end up true). Locate the home
method and update the authority-check logic that produces isSysAdmin and isAdmin
accordingly.
src/main/resources/templates/error/error.html (1)

70-94: LGTM — auth-aware header and CTAs are correctly wired.

The header is now wrapped in a th:block sec:authorize="isAuthenticated()" (so the previously-flagged th:replace precedence bypass is no longer reachable), the CTAs are gated by sec:authorize for authenticated vs anonymous users, and the .no-nav class trick keeps the layout vertically centered when no nav is rendered.

One small consideration: if errorTitle/errorMessage aren't populated by the upstream handler (e.g., a Spring Security filter-chain failure that bypasses GlobalExceptionHandler), lines 80–81 will render empty strings. Adding a default via Elvis would harden the page:

♻️ Optional null-safe defaults
-        <div class="error-code" th:text="${errorTitle}">⚠️Error.</div>
-        <h1 th:text="${errorMessage}">Something went wrong</h1>
+        <div class="error-code" th:text="${errorTitle ?: 'Error'}">⚠️Error.</div>
+        <h1 th:text="${errorMessage ?: 'Something went wrong'}">Something went wrong</h1>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/error/error.html` around lines 70 - 94, Add
null-safe defaults for the errorTitle and errorMessage bindings so the page
shows sensible text when the upstream handler doesn't populate them: update the
th:text on the element rendering errorTitle (the element with class
"error-code") to use the Elvis/default operator with a fallback like "⚠️ Error"
and update the th:text on the h1 rendering errorMessage to use an Elvis/default
fallback like "Something went wrong"; keep the same attributes (th:text) and
security gating unchanged.
src/test/java/org/example/visacasemanagementsystem/audit/service/UserLogServiceTest.java (1)

340-361: LGTM — solid coverage of the new Specification path.

The eventType / from / to / all-filters cases correctly capture the built Specification and verify the predicate composition by replaying it against mocked Criteria API components. One thing worth flagging as an awareness item rather than a defect: the all-filters test at Line 348 stubs root.get(...) to return paths in a fixed sequence (eventTypePath, timeStampPath, timeStampPath), so it implicitly couples the test to the order in which findFiltered calls root.get(...) and to the order of cb.and(...) chaining. If the service is later reordered to combine predicates in a different sequence (e.g., from/to before eventType), this test will fail in a way that looks like a regression even though behavior is unchanged. Worth a comment noting the assumption, or a refactor toward argThat-style matchers keyed on the metamodel attribute.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/audit/service/UserLogServiceTest.java`
around lines 340 - 361, The test is brittle because it stubs root.get(...) and
cb.and(...) with a fixed sequence (eventTypePath, timeStampPath, timeStampPath)
which couples the specCaptor verification to call order; update the test to
either add a clear comment describing that assumption near the specCaptor
verification or refactor the stubbing to match by attribute rather than position
(use argument matchers when stubbing root.get(...) and
cb.equal/greaterThanOrEqualTo/lessThanOrEqualTo so you return
eventTypePath/timeStampPath based on the passed SingularAttribute, or use
argThat on the metamodel attribute), ensuring
specCaptor.getValue().toPredicate(root, query, cb) is driven by attribute-aware
stubs instead of sequence-dependent returns for root.get, and keep references to
root.get, cb.equal, cb.greaterThanOrEqualTo, cb.lessThanOrEqualTo, and
specCaptor in your changes.
src/main/resources/static/css/components.css (1)

129-153: Heavy global restyling of label and input/select/textarea may unintentionally bleed into pages outside this PR's scope.

label { display: block; text-transform: uppercase; … } and the input, select, textarea block (which forces width: 100%, resize: none, and a dark-theme color scheme) apply to every form element on any page that links components.css. A few practical consequences worth being aware of:

  1. Any inline form (e.g., a label adjacent to a checkbox/radio) loses inline alignment because of the forced display: block.
  2. resize: none on textarea is then overridden ad-hoc in details.html (resize: vertical) — the inverse of the usual default — making it easy to forget on new templates.
  3. Future widget styles (e.g., the recommended .file-input-label from the prior file-input discussion) will inherit the uppercase 11px reset and need explicit overrides (note: you've already preempted this with text-transform: none in the suggested .file-input-label rule).

Not a blocker — this is a deliberate "shared = canonical" trade-off — but consider scoping the broad selectors to a wrapper class (e.g., .form-group label, .form-group input) so opt-in is explicit and the global cascade stays small.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/css/components.css` around lines 129 - 153, The
current global selectors (label and input, select, textarea) are too broad;
restrict them by scoping to a wrapper such as .form-group (e.g., change label to
.form-group label and input, select, textarea to .form-group input, .form-group
select, .form-group textarea), move properties that break defaults
(display:block, width:100%, resize:none, color) into that scoped rule, and
ensure existing exceptions like .file-input-label and details.html’s textarea
override remain or are updated to .form-group-specific rules so other pages
won’t inherit these styles unintentionally.
src/main/resources/templates/fragments/cases.html (1)

50-50: Use .name() instead of relying on enum toString() for the CSS class.

#strings.toLowerCase(visa.visaStatus) works because Thymeleaf coerces the enum via toString(), which by default returns name(). If anyone overrides VisaStatus.toString() later (a common practice for human-readable labels), the CSS class names silently break (e.g., status-granted becomes status-visa granted). Use .name() to lock the contract, mirroring the explicit pattern used in log/visa.html and log/user.html:

♻️ Suggested change
-                <span class="status"
-                      th:classappend="${'status-' + `#strings.toLowerCase`(visa.visaStatus)}"
-                      th:text="${visa.visaStatus}">ASSIGNED</span>
+                <span class="status"
+                      th:classappend="${'status-' + `#strings.toLowerCase`(visa.visaStatus.name())}"
+                      th:text="${visa.visaStatus}">ASSIGNED</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/cases.html` at line 50, The Thymeleaf
expression currently coerces the enum via toString() in the th:classappend
attribute (th:classappend="${'status-' +
`#strings.toLowerCase`(visa.visaStatus)}"), which can break if
VisaStatus.toString() is overridden; change the expression to call the enum's
name() explicitly and pass that into the lowercase helper (use
visa.visaStatus.name() with `#strings.toLowerCase`) so the CSS class generation
uses the stable enum name (referencing the th:classappend attribute and
visa.visaStatus).
src/main/resources/templates/log/visa.html (1)

30-37: Use .name() for <option> values to prevent future binding failures if toString() is overridden.

Currently, th:value="${type}" serializes the enum via toString(), which works today because both VisaEventType and UserEventType use the default Enum.toString() (returning the enum name). However, if either enum later overrides toString() to return a human-readable format (e.g., "Created"), the rendered <option value="Created"> will no longer round-trip through Spring's enum converter on form submit, and eventType will silently bind to null instead of the selected value.

The display text can remain human-readable, but the option value should explicitly use the enum name to lock the wire format:

♻️ Suggested change (apply same to `log/user.html`)
                 <select id="eventType" name="eventType">
                     <option value="" th:selected="${selectedEventType == null}">All</option>
                     <option th:each="type : ${eventTypes}"
-                            th:value="${type}"
+                            th:value="${type.name()}"
                             th:text="${type}"
                             th:selected="${selectedEventType != null and selectedEventType.name() == type.name()}">
                     </option>
                 </select>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/log/visa.html` around lines 30 - 37, The select
option values currently use th:value="${type}" which relies on Enum.toString()
and can break binding if enums override toString(); change the value binding to
use the enum name (th:value="${type.name()}") while keeping the display text
(th:text) as-is for human-readable labels, and apply the same update to the
analogous select in log/user.html; target the select with id/name "eventType"
and enums like VisaEventType/UserEventType when making this change.
src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java (1)

62-81: Three-list showCases wiring is correct, but meId is fetched twice.

principal.getUserId() is invoked at Line 66 (inside findById) and again at Line 69 to populate meId. They will always return the same value, so this is harmless — just slightly redundant. Optional cleanup: pull meId first and reuse it for the findById call:

♻️ Optional cleanup
-    public String showCases(`@AuthenticationPrincipal` UserPrincipal principal, Model model) {
-        UserDTO user = userService.findById(principal.getUserId())
-                .orElseThrow(() -> new EntityNotFoundException("User not found."));
-
-        Long meId = principal.getUserId();
-
-        List<VisaDTO> openCases = visaService.findOpenCasesByHandler(meId);
+    public String showCases(`@AuthenticationPrincipal` UserPrincipal principal, Model model) {
+        Long meId = principal.getUserId();
+        UserDTO user = userService.findById(meId)
+                .orElseThrow(() -> new EntityNotFoundException("User not found."));
+
+        List<VisaDTO> openCases = visaService.findOpenCasesByHandler(meId);

Also worth confirming — given templates/visa/cases.html accesses ${currentUser.fullName} (no parens) while my-applications.html uses ${currentUser.fullName()} — that both expression styles resolve correctly against UserDTO (depends on whether fullName is exposed as a record accessor or a JavaBean property). If UserDTO is a Java record, Thymeleaf's OGNL/SpringEL can usually resolve both forms, but inconsistency invites template-rendering bugs later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`
around lines 62 - 81, In showCases, avoid calling principal.getUserId() twice:
read meId = principal.getUserId() first and reuse meId in the call to
userService.findById(...) and the subsequent visaService methods (update
references to UserDTO user = userService.findById(meId)... and keep model
population unchanged); also verify UserDTO exposes a consistent fullName
accessor (getter or record accessor) so templates using ${currentUser.fullName}
and ${currentUser.fullName()} both resolve or update templates to one consistent
form.
src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java (1)

130-135: Stub may not match the actual Pageable produced by the controller.

You stub findFiltered(eq(GRANTED), eq(expectedFrom), eq(expectedTo), any(Pageable.class)) to return a non-empty page, but in the "no filters" test (Lines 103–104) you stub the same method with isNull() matchers. Because Mockito picks the most recent matching stub, this is OK in isolation, but be aware that if any of the eq-matched arguments don't match (e.g., the controller someday switches end-of-day to LocalTime.MAX.minusNanos(...) or an instant truncation), this stub silently returns null and the controller will NPE rendering the page — making the failure look unrelated to the actual mismatch. Consider using when(...).thenReturn(page) with any() for date args plus a separate ArgumentCaptor<LocalDateTime> assertion, so a regression on the conversion produces a clear, targeted assertion failure rather than a downstream NPE.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java`
around lines 130 - 135, The test stubs visaLogService.findFiltered(...) with
strict eq(expectedFrom)/eq(expectedTo) which can silently stop matching if the
controller alters date conversion; change the stub in LogViewControllerTest to
use more flexible matchers (e.g., any(LocalDateTime.class) or any()) for the
from/to arguments when calling when(visaLogService.findFiltered(...)). Return
the prepared Page (PageImpl) from that stub, and add an
ArgumentCaptor<LocalDateTime> (capturing the from/to arguments passed into
visaLogService.findFiltered) to assert the controller's actual
conversion/rounding behavior explicitly so regressions fail with a clear
assertion instead of an NPE; keep the other matchers (e.g.,
eq(VisaEventType.GRANTED) or any(Pageable.class)) as appropriate.
src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java (2)

51-51: defaultValue = "" + DEFAULT_PAGE_SIZE relies on compile-time constant folding.

Because DEFAULT_PAGE_SIZE is static final int initialized with a literal, JLS §15.28 makes "" + DEFAULT_PAGE_SIZE a constant expression, so the annotation is legal here. Worth being aware though that if DEFAULT_PAGE_SIZE ever stops being a compile-time constant (e.g., computed from a config value) this annotation will stop compiling. A small alternative is to define a String DEFAULT_PAGE_SIZE_STR = "20" constant alongside the int and reference that directly to make the dependency explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java`
at line 51, The annotation uses defaultValue = "" + DEFAULT_PAGE_SIZE which
relies on compile-time constant folding; replace this with an explicit String
constant to avoid future compile errors: add a String constant (e.g.,
DEFAULT_PAGE_SIZE_STR = "20") alongside the existing static final int
DEFAULT_PAGE_SIZE and update the `@RequestParam` on the size parameter in
LogViewController (the method using the size param) to use
DEFAULT_PAGE_SIZE_STR; ensure both constants are kept in sync or derive the int
from the String if needed.

26-28: Belt-and-braces authorization — fine, but note the redundancy.

SecurityConfig.requestMatchers("/log/**").hasRole("SYSADMIN") already gates these URLs, so the class-level @PreAuthorize("hasRole('SYSADMIN')") is defensive duplication. Not a problem (extra layer of safety in case SecurityConfig is ever loosened), just worth documenting the intent so a future maintainer doesn't drop one of the two guards thinking it's dead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java`
around lines 26 - 28, The class-level `@PreAuthorize`("hasRole('SYSADMIN')") on
LogViewController is intentionally duplicated with
SecurityConfig.requestMatchers("/log/**").hasRole("SYSADMIN") as a defensive
guard; add a short clarifying comment above the LogViewController class (or
adjacent to the `@PreAuthorize` annotation) stating that this redundancy is
deliberate to provide an extra layer of authorization protection in case
SecurityConfig changes, so future maintainers don't remove one of the checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java`:
- Around line 77-84: The logout POST is failing because the logout form in the
header fragment doesn't include the CSRF token; update the form in
src/main/resources/templates/fragments/header.html that posts to /user/logout
(used by the logout() method in UserViewController) to include a hidden input
with the CSRF parameter name and token (use _csrf.parameterName and _csrf.token)
so the Spring Security CSRF filter accepts the POST; ensure the form action
remains th:action="@{/user/logout}" and method="post".

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`:
- Around line 240-274: The PR broadened case-management endpoints approveVisa,
requestMoreInformation, rejectVisa, and assignCaseToHandler to
hasAnyRole('ADMIN','SYSADMIN'), which may violate separation-of-duties; if
SYSADMIN should remain audit-only, change those `@PreAuthorize` annotations back
to hasRole('ADMIN') on the four methods (approveVisa, requestMoreInformation,
rejectVisa, assignCaseToHandler) and ensure SYSADMIN retains read-only access
via the ApplicationViewController.home/view endpoints instead of mutation
rights; if the broader access was intentional, add a short code comment near
these methods documenting the product decision and update authorization tests to
reflect SYSADMIN can perform these actions.

In `@src/main/resources/static/css/components.css`:
- Around line 164-167: The current input:disabled rule reduces opacity to 0.4
which makes text (derived from --color-text-secondary over --color-input-bg)
fail WCAG contrast; update the input:disabled CSS rule to use higher opacity
(about 0.6) and/or explicitly set a higher-contrast color or distinct background
for disabled inputs (reference the variables --color-text-secondary and
--color-input-bg) so disabled-but-readable fields remain >=4.5:1 contrast —
modify the input:disabled selector to increase opacity and optionally add a
background-color or color override to preserve legibility.
- Around line 187-198: The CSS currently targets input[type="file"]::before
which is unsupported in Firefox/Safari; replace the nonstandard pseudo-element
usage by applying styling to the standard ::file-selector-button and keep the
existing ::-webkit-file-upload-button fallback, updating rules that reference
input[type="file"]::before to instead target
input[type="file"]::file-selector-button (and retain
::-webkit-file-upload-button for WebKit) so the fake button styling (background,
border, padding, margin-right, color, font-weight, font-size) applies
cross-browser.

In `@src/main/resources/static/css/tokens.css`:
- Around line 41-43: The variables --color-text-dimmer (`#555`) and
--color-placeholder (`#444`) do not meet WCAG AA contrast for normal text; update
--color-text-dimmer to at least `#767676` and raise --color-placeholder to a value
that yields ≥4.5:1 against dark backgrounds (or restrict its use to
non-essential decorative placeholders), then test the updated tokens against the
affected selectors (.empty-state, .site-nav .nav-user, ::placeholder and the "No
comments yet." message) to ensure they meet the contrast threshold or are only
used where WCAG AAA is acceptable for small text.

In `@src/main/resources/templates/fragments/cases.html`:
- Around line 53-54: The fragment unconditionally calls `#temporals.format` on
visa.travelDate and visa.updatedAt which throws when those fields are null;
update the two <td> cells to guard the calls (mirror the existing pattern used
elsewhere) by checking visa.travelDate != null and visa.updatedAt != null before
calling `#temporals.format` (e.g., use a conditional/th:if or a
ternary/Elvis-style expression in the th:text) so formatting runs only when the
corresponding visa.travelDate or visa.updatedAt is present.

In `@src/main/resources/templates/log/user.html`:
- Around line 73-77: The span element rendering user log event chips currently
uses the base "status" class and a th:classappend that produces
"status-<userEventType>" names (see the span with class="status" and the
th:classappend="${log.userEventType != null ? 'status-' +
`#strings.toLowerCase`(log.userEventType.name()) : ''}" expression), but
components.css lacks rules for UserEventType values (CREATED, UPDATED, DELETED,
AUTHORIZATION_CHANGED); either add corresponding CSS rules (.status-created,
.status-updated, .status-deleted, .status-authorization_changed) to
components.css to style these pills, or change the span to use the existing
styled badge class (replace the base "status" with "event-badge" and keep the
th:classappend or remove it) so user-log events render with the intended styled,
monospace audit chip.

In `@src/main/resources/templates/user/list.html`:
- Line 26: The span rendering the total users ("user-count" with
th:text="${`#lists.size`(users) + ' users total'}") always uses "users" and so
shows "1 users total"; change the expression to perform pluralization—either
replace the current th:text with a conditional/ternary expression that uses
"user" when `#lists.size`(users) == 1 and "users" otherwise, or route through a
message bundle key (e.g. users.total) with the count as a parameter and
pluralization handled in messages; update the span's th:text to use that
conditional or message key accordingly.

In `@src/main/resources/templates/visa/apply-form.html`:
- Line 33: Hidden applicantId input creates a tamper vector because VisaService
currently reads dto.applicantId instead of the trusted principal userId; remove
the hidden input from the template and make the service use the
controller-supplied userId argument (update VisaService.applyForVisa to read the
applicant from the userId parameter, not CreateVisaDTO.applicantId), or
alternatively remove applicantId from CreateVisaDTO and have the controller
(VisaViewController) populate it from principal.getUserId() before calling
VisaService.applyForVisa; ensure only the trusted userId source is used and
remove reliance on form-submitted applicantId.

In
`@src/test/java/org/example/visacasemanagementsystem/audit/service/VisaLogServiceTest.java`:
- Around line 326-331: The mocks for root.get(...) and the chained cb.and(...)
calls rely on a specific evaluation order (root.get called for eventTypePath
then timeStampPath twice, and cb.and(eqPred, gtePred) then cb.and(and1,
ltePred)), so either add a one-line comment above these when(...) stubs
explaining that the specification builder evaluates checks in that sequence
(event-type → from → to) or replace the multi-value thenReturn usage with
Mockito InOrder verification on root.get and cb.and to assert the exact call
order (referencing root.get, cb.equal, cb.greaterThanOrEqualTo,
cb.lessThanOrEqualTo, and cb.and along with eqPred/gtePred/ltePred/and1/and2) so
the test explicitly documents/verifies the order dependency.

---

Nitpick comments:
In
`@src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java`:
- Around line 27-40: The home method currently iterates
principal.getAuthorities() twice to compute isSysAdmin and isAdmin; collapse
this into a single pass over principal.getAuthorities() (inside home) that sets
two booleans (isSysAdmin, isAdmin) as it sees each authority so you avoid double
iteration while preserving the existing preference logic (ensure SYSADMIN
redirect still takes precedence if both flags end up true). Locate the home
method and update the authority-check logic that produces isSysAdmin and isAdmin
accordingly.

In
`@src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java`:
- Line 51: The annotation uses defaultValue = "" + DEFAULT_PAGE_SIZE which
relies on compile-time constant folding; replace this with an explicit String
constant to avoid future compile errors: add a String constant (e.g.,
DEFAULT_PAGE_SIZE_STR = "20") alongside the existing static final int
DEFAULT_PAGE_SIZE and update the `@RequestParam` on the size parameter in
LogViewController (the method using the size param) to use
DEFAULT_PAGE_SIZE_STR; ensure both constants are kept in sync or derive the int
from the String if needed.
- Around line 26-28: The class-level `@PreAuthorize`("hasRole('SYSADMIN')") on
LogViewController is intentionally duplicated with
SecurityConfig.requestMatchers("/log/**").hasRole("SYSADMIN") as a defensive
guard; add a short clarifying comment above the LogViewController class (or
adjacent to the `@PreAuthorize` annotation) stating that this redundancy is
deliberate to provide an extra layer of authorization protection in case
SecurityConfig changes, so future maintainers don't remove one of the checks.

In
`@src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java`:
- Around line 45-62: No functional change required—findFiltered in
UserLogService is correct; optionally extract the repeated
predicate-construction used by UserLogService#findFiltered and
VisaLogService#findFiltered into a small shared helper (e.g.,
SpecificationUtils.buildEqualsOrRange) that accepts a metamodel attribute
(UserLog_.userEventType/UserLog_.timeStamp) and a value or range bounds and
returns a Specification to be and()-chained; implement the helper to produce the
same lambdas (equal, greaterThanOrEqualTo, lessThanOrEqualTo) so both services
can call the helper instead of duplicating the spec construction.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`:
- Around line 62-81: In showCases, avoid calling principal.getUserId() twice:
read meId = principal.getUserId() first and reuse meId in the call to
userService.findById(...) and the subsequent visaService methods (update
references to UserDTO user = userService.findById(meId)... and keep model
population unchanged); also verify UserDTO exposes a consistent fullName
accessor (getter or record accessor) so templates using ${currentUser.fullName}
and ${currentUser.fullName()} both resolve or update templates to one consistent
form.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java`:
- Around line 467-502: Extract the per-call lists into class-level constants to
avoid reallocations: define private static final List<VisaStatus> OPEN_STATUSES
= List.of(VisaStatus.ASSIGNED, VisaStatus.INCOMPLETE) and private static final
List<VisaStatus> HANDLED_STATUSES = List.of(VisaStatus.GRANTED,
VisaStatus.REJECTED) in VisaService, then replace the local openStatuses and
handledStatuses variables in findOpenCasesByHandler(Long) and
findHandledCasesByHandler(Long) to use OPEN_STATUSES and HANDLED_STATUSES
respectively (keep the Sort.by("updatedAt").descending() and mapping via
visaMapper::toDTO unchanged).

In `@src/main/resources/static/css/components.css`:
- Around line 129-153: The current global selectors (label and input, select,
textarea) are too broad; restrict them by scoping to a wrapper such as
.form-group (e.g., change label to .form-group label and input, select, textarea
to .form-group input, .form-group select, .form-group textarea), move properties
that break defaults (display:block, width:100%, resize:none, color) into that
scoped rule, and ensure existing exceptions like .file-input-label and
details.html’s textarea override remain or are updated to .form-group-specific
rules so other pages won’t inherit these styles unintentionally.

In `@src/main/resources/templates/error/error.html`:
- Around line 70-94: Add null-safe defaults for the errorTitle and errorMessage
bindings so the page shows sensible text when the upstream handler doesn't
populate them: update the th:text on the element rendering errorTitle (the
element with class "error-code") to use the Elvis/default operator with a
fallback like "⚠️ Error" and update the th:text on the h1 rendering errorMessage
to use an Elvis/default fallback like "Something went wrong"; keep the same
attributes (th:text) and security gating unchanged.

In `@src/main/resources/templates/fragments/cases.html`:
- Line 50: The Thymeleaf expression currently coerces the enum via toString() in
the th:classappend attribute (th:classappend="${'status-' +
`#strings.toLowerCase`(visa.visaStatus)}"), which can break if
VisaStatus.toString() is overridden; change the expression to call the enum's
name() explicitly and pass that into the lowercase helper (use
visa.visaStatus.name() with `#strings.toLowerCase`) so the CSS class generation
uses the stable enum name (referencing the th:classappend attribute and
visa.visaStatus).

In `@src/main/resources/templates/fragments/header.html`:
- Around line 41-43: The logout POST form (form with th:action="@{/user/logout}"
and class "logout-form" containing button "btn-signout") should include an
explicit CSRF hidden input for consistency with other forms; add an input using
th:name="${_csrf.parameterName}" and th:value="${_csrf.token}" inside that form
(or, if you intentionally rely on Thymeleaf/Spring Security auto-injection, add
a short comment in the template explaining that auto-injection is relied upon)
so all POST forms are standardized.

In `@src/main/resources/templates/log/visa.html`:
- Around line 30-37: The select option values currently use th:value="${type}"
which relies on Enum.toString() and can break binding if enums override
toString(); change the value binding to use the enum name
(th:value="${type.name()}") while keeping the display text (th:text) as-is for
human-readable labels, and apply the same update to the analogous select in
log/user.html; target the select with id/name "eventType" and enums like
VisaEventType/UserEventType when making this change.

In `@src/main/resources/templates/visa/edit-form.html`:
- Line 79: This template mixes property-style SpEL with the rest of the PR;
update all record field accesses in edit-form.html to use method-style accessors
to match the other templates (e.g., change occurrences of currentUser.fullName,
updateVisaDto.visaStatus, updateVisaDto.id, visa.id and any downloadUrls/s3Keys
references to currentUser.fullName(), updateVisaDto.visaStatus(),
updateVisaDto.id(), visa.id() and downloadUrls()/s3Keys() respectively) so the
visa templates consistently use the method-style record accessors.
- Around line 137-141: The inline th:onclick that uses confirm(...) and
document.getElementById('delete-doc-' + ${iter.index}).submit() is
CSP-unfriendly and couples the button to a distant hidden form; instead move the
delete <form> (the one currently rendered as the hidden sibling with id
'delete-doc-<index>') into the same document row as the remove <button> and
replace the inline th:onclick wiring with a plain submit button inside that form
(use <button type="submit">) and, if you still want a JS confirmation, attach a
non-inline event listener (or use a data-confirm attribute that a central script
handles) rather than using th:onclick; update references to
delete-doc-${iter.index} and the button markup in the template accordingly.

In `@src/main/resources/templates/visa/my-applications.html`:
- Around line 33-68: The template currently renders the <table> (with headers)
unconditionally and separately renders the empty-state when ${visas} is empty,
causing an empty table to appear; change the structure so the table (the <table>
element that iterates th:each="visa : ${visas}" and the header row) is rendered
only when the list is not empty and the "You have no applications yet." message
is rendered when `#lists.isEmpty`(visas) is true — e.g., wrap the entire table
with th:if="${!#lists.isEmpty(visas)}" (or move the th:if from the empty-state
to the table inversion) and keep the existing empty-state div with
th:if="${`#lists.isEmpty`(visas)}"; ensure the th:each on visa and the view link
href (@{/visa/{id}(id=${visa.id()})}) remain unchanged.

In
`@src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java`:
- Around line 130-135: The test stubs visaLogService.findFiltered(...) with
strict eq(expectedFrom)/eq(expectedTo) which can silently stop matching if the
controller alters date conversion; change the stub in LogViewControllerTest to
use more flexible matchers (e.g., any(LocalDateTime.class) or any()) for the
from/to arguments when calling when(visaLogService.findFiltered(...)). Return
the prepared Page (PageImpl) from that stub, and add an
ArgumentCaptor<LocalDateTime> (capturing the from/to arguments passed into
visaLogService.findFiltered) to assert the controller's actual
conversion/rounding behavior explicitly so regressions fail with a clear
assertion instead of an NPE; keep the other matchers (e.g.,
eq(VisaEventType.GRANTED) or any(Pageable.class)) as appropriate.

In
`@src/test/java/org/example/visacasemanagementsystem/audit/service/UserLogServiceTest.java`:
- Around line 340-361: The test is brittle because it stubs root.get(...) and
cb.and(...) with a fixed sequence (eventTypePath, timeStampPath, timeStampPath)
which couples the specCaptor verification to call order; update the test to
either add a clear comment describing that assumption near the specCaptor
verification or refactor the stubbing to match by attribute rather than position
(use argument matchers when stubbing root.get(...) and
cb.equal/greaterThanOrEqualTo/lessThanOrEqualTo so you return
eventTypePath/timeStampPath based on the passed SingularAttribute, or use
argThat on the metamodel attribute), ensuring
specCaptor.getValue().toPredicate(root, query, cb) is driven by attribute-aware
stubs instead of sequence-dependent returns for root.get, and keep references to
root.get, cb.equal, cb.greaterThanOrEqualTo, cb.lessThanOrEqualTo, and
specCaptor in your changes.

In
`@src/test/java/org/example/visacasemanagementsystem/audit/service/VisaLogServiceTest.java`:
- Around line 207-222: Tests in VisaLogServiceTest repeat the same Criteria
mocks (Root, CriteriaQuery, CriteriaBuilder, Path, Predicate) and the
root.get(nullable(SingularAttribute.class)) stub across multiple cases; extract
that repeated setup into a small helper (e.g., a private method like
setupCriteriaMocks or a CriteriaMocks fixture) that creates and stubs Root,
CriteriaQuery, CriteriaBuilder, Path and returns the mocks (or assigns fields)
so each test can call the helper and then only assert the specific behavior of
specCaptor.getValue().toPredicate(root, query, cb) and verifications of cb.equal
/ greaterThanOrEqualTo / lessThanOrEqualTo; update tests at the other locations
(around lines noted) to use the helper to remove the duplicated when(...) and
mock(...) lines while keeping existing assertions and specCaptor usage.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`:
- Around line 513-525: createMockUser currently both returns a UserDTO and
mutates global SecurityContextHolder via
SecurityContextHolder.getContext().setAuthentication(...), which is misleading
and conflicts with `@WithMockUser`; split it into a pure factory (e.g.,
createMockUserDTO(Long id, UserAuthorization role) that only constructs and
returns UserDTO/User objects) and a side-effecting helper (e.g.,
mockAuthenticatedAs(User user) or mockAuthenticatedAs(UserDTO dto) that creates
a UserPrincipal and TestingAuthenticationToken and calls
SecurityContextHolder.getContext().setAuthentication(...)); update tests that
only need the DTO to call the pure factory and tests that need to set
authentication to call the new mockAuthenticatedAs helper (or remove the manual
SecurityContextHolder writes and rely on `@WithMockUser` / with(user(...))
consistently).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 449a6dbd-34d0-451f-997a-8959120739ea

📥 Commits

Reviewing files that changed from the base of the PR and between 325552a and db4bf3b.

📒 Files selected for processing (40)
  • pom.xml
  • src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java
  • src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java
  • src/main/java/org/example/visacasemanagementsystem/audit/repository/UserLogRepository.java
  • src/main/java/org/example/visacasemanagementsystem/audit/repository/VisaLogRepository.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java
  • src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java
  • src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java
  • src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java
  • src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java
  • src/main/resources/static/css/components.css
  • src/main/resources/static/css/tokens.css
  • src/main/resources/templates/dashboard/admin.html
  • src/main/resources/templates/dashboard/applicant.html
  • src/main/resources/templates/dashboard/sysadmin.html
  • src/main/resources/templates/error/error.html
  • src/main/resources/templates/fragments/cases.html
  • src/main/resources/templates/fragments/header.html
  • src/main/resources/templates/log/user.html
  • src/main/resources/templates/log/visa.html
  • src/main/resources/templates/profile/edit.html
  • src/main/resources/templates/profile/view.html
  • src/main/resources/templates/user/list.html
  • src/main/resources/templates/user/login.html
  • src/main/resources/templates/user/signup.html
  • src/main/resources/templates/visa/apply-form.html
  • src/main/resources/templates/visa/cases.html
  • src/main/resources/templates/visa/dashboard.html
  • src/main/resources/templates/visa/details.html
  • src/main/resources/templates/visa/edit-form.html
  • src/main/resources/templates/visa/my-applications.html
  • src/test/java/org/example/visacasemanagementsystem/audit/controller/LogViewControllerTest.java
  • src/test/java/org/example/visacasemanagementsystem/audit/service/UserLogServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/audit/service/VisaLogServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java
💤 Files with no reviewable changes (4)
  • src/main/resources/templates/dashboard/sysadmin.html
  • src/main/resources/templates/dashboard/applicant.html
  • src/main/resources/templates/visa/dashboard.html
  • src/main/resources/templates/dashboard/admin.html

Comment thread src/main/resources/static/css/components.css
Comment thread src/main/resources/static/css/components.css Outdated
Comment thread src/main/resources/static/css/tokens.css Outdated
Comment thread src/main/resources/templates/fragments/cases.html Outdated
Comment on lines +73 to +77
<td>
<span class="status"
th:classappend="${log.userEventType != null ? 'status-' + #strings.toLowerCase(log.userEventType.name()) : ''}"
th:text="${log.userEventType}">CREATED</span>
</td>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Heads-up: status-* classes for UserEventType aren't defined in components.css.

components.css only defines .status-submitted/unassigned/assigned/incomplete/granted/rejected. For UserEventType (CREATED, UPDATED, DELETED, AUTHORIZATION_CHANGED), th:classappend will produce class names like status-created and status-authorization_changed, none of which are styled — so user-log event chips render as the unstyled base .status pill. If that's intentional (treating user-log events as plain badges), no change needed. Otherwise, either add .status-created/updated/deleted/authorization_changed rules to components.css or use the existing .event-badge class instead of .status here for a styled, monospace audit chip.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/log/user.html` around lines 73 - 77, The span
element rendering user log event chips currently uses the base "status" class
and a th:classappend that produces "status-<userEventType>" names (see the span
with class="status" and the th:classappend="${log.userEventType != null ?
'status-' + `#strings.toLowerCase`(log.userEventType.name()) : ''}" expression),
but components.css lacks rules for UserEventType values (CREATED, UPDATED,
DELETED, AUTHORIZATION_CHANGED); either add corresponding CSS rules
(.status-created, .status-updated, .status-deleted,
.status-authorization_changed) to components.css to style these pills, or change
the span to use the existing styled badge class (replace the base "status" with
"event-badge" and keep the th:classappend or remove it) so user-log events
render with the intended styled, monospace audit chip.

Comment thread src/main/resources/templates/user/list.html Outdated

<form th:action="@{/visas/apply}" th:object="${createVisaDTO}" method="post" enctype="multipart/form-data">
<form th:action="@{/visa/apply}" th:object="${createVisaDTO}" method="post" enctype="multipart/form-data">
<input type="hidden" name="applicantId" th:value="${createVisaDTO.applicantId}" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether VisaService.applyForVisa ever reads dto.applicantId() vs. the userId argument.
fd -t f 'VisaService.java' --exec rg -nP -C3 'applyForVisa|applicantId\(\)|\.applicantId\b' {}

Repository: ithsjava25/visa-case-management-system

Length of output: 566


Remove the hidden applicantId field — the service reads it instead of the trusted userId parameter.

The submit handler in VisaViewController extracts the applicant from principal.getUserId() and passes it as a separate userId argument to VisaService.applyForVisa(CreateVisaDTO dto, Long userId, String s3Key). However, VisaService ignores that argument and reads dto.applicantId() directly from the DTO (line 87 of VisaService.java), which comes from the hidden input you're posting. This creates a tamper vector where a user can submit an application with any applicantId, effectively applying for visas on behalf of other users. Either:

  1. Remove the applicantId field from CreateVisaDTO and have the service use the userId parameter, or
  2. If the DTO field must remain, remove the hidden input and ensure the service uses only the userId argument.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/apply-form.html` at line 33, Hidden
applicantId input creates a tamper vector because VisaService currently reads
dto.applicantId instead of the trusted principal userId; remove the hidden input
from the template and make the service use the controller-supplied userId
argument (update VisaService.applyForVisa to read the applicant from the userId
parameter, not CreateVisaDTO.applicantId), or alternatively remove applicantId
from CreateVisaDTO and have the controller (VisaViewController) populate it from
principal.getUserId() before calling VisaService.applyForVisa; ensure only the
trusted userId source is used and remove reliance on form-submitted applicantId.

Critical
- Remove tamperable applicantId from CreateVisaDTO; visa ownership is
  derived solely from the authenticated principal. Update the apply
  form and all CreateVisaDTO call sites accordingly.
- Add explicit CSRF token input to the /user/logout form and the
  apply-visa form to match the codebase's established pattern.
- Document the deliberate SYSADMIN-broad-access design on the four
  case-mutation endpoints in VisaViewController.

Accessibility / UX
- Bump --color-text-dimmer and --color-placeholder to #767676 for
  WCAG AA contrast against the dark surfaces.
- Raise disabled-input opacity from 0.4 to 0.6 (was below AA for
  normal text).
- Replace nonstandard input[type=file]::before with the standard
  ::file-selector-button rule (plus a ::-webkit-file-upload-button
  fallback) so file inputs render correctly in Firefox and Safari.
- Add status pill rules for user-event log entries (CREATED,
  UPDATED, DELETED, AUTHORIZATION_CHANGED).
- Pluralize the user-list count.
- Null-guard date formatting in fragments/cases.html and switch the
  status classappend to enum.name() instead of toString.
- error.html falls back to "Error" / "Something went wrong" when the
  model attributes are missing.
- log/visa.html and log/user.html: bind <option> values to
  enum.name() so the SYSADMIN filter stays deterministic.
- visa/my-applications.html: only render the <table> when the list
  is non-empty (was showing an empty header above the empty-state).
- visa/edit-form.html: replace inline th:onclick with a data-form-id
  attribute + delegated .js-delete-doc listener for CSP compatibility.

Refactors
- VisaService: extract OPEN_STATUSES / HANDLED_STATUSES as class-level
  constants; eliminate per-request List.of(...) allocations.
- LogViewController: extract DEFAULT_PAGE_SIZE_STR so the
  @RequestParam defaultValue cannot drift from DEFAULT_PAGE_SIZE; add
  a class-level note explaining the deliberate redundancy between
  @PreAuthorize and the SecurityConfig URL rule.
- ApplicationViewController.home: collapse two stream walks over the
  authorities collection into a single-pass for-loop.

Tests
- Update CreateVisaDTO call sites in VisaServiceTest,
  VisaServiceIntegrationTest, and VisaMapperTest to the 4-arg form.
- VisaLogServiceTest and UserLogServiceTest: document the ordering
  dependency of the multi-value thenReturn stubs on the Specification
  builder's evaluation sequence (eventType -> from -> to).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/resources/templates/visa/edit-form.html (1)

128-143: ⚠️ Potential issue | 🔴 Critical

Fix downloadUrls population in VisaMapper—currently hardcoded to empty list.

The mapper's toDTO() method (line 32 in VisaMapper.java) hardcodes downloadUrls to List.of(), making the "Current Passport Documents" section unreachable in the template. The template's condition at line 128 checks th:if="${not #lists.isEmpty(visa.downloadUrls)}", but this list will always be empty.

Additionally, the original concern about parallelism remains valid: downloadUrls and s3Keys are iterated separately at lines 131 and 164 with matching indices. If these collections ever differ in length or ordering, document deletion will target the wrong S3 key.

Recommended fix: Populate downloadUrls in the mapper (likely from visa.getS3Keys() or a separate method that generates pre-signed URLs), and ensure both lists remain strictly parallel or refactor to iterate a single zipped structure (e.g., a list of {downloadUrl, s3Key} pairs).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/edit-form.html` around lines 128 - 143,
VisaMapper.toDTO() currently sets downloadUrls to an empty list which prevents
the template check th:if="${not `#lists.isEmpty`(visa.downloadUrls)}" from ever
rendering; change VisaMapper.toDTO() to populate downloadUrls from the source S3
keys (e.g., visa.getS3Keys()) or by generating presigned URLs via your existing
S3 helper, and return those URLs in the DTO instead of List.of(). Also eliminate
fragile parallel iteration by either (a) adding a single documents collection on
the DTO (e.g., List<DocumentDTO> with fields downloadUrl and s3Key) and mapping
visa.getS3Keys() -> List<DocumentDTO>, or (b) ensure downloadUrls and s3Keys are
created in the same order and same length inside VisaMapper.toDTO() so the
template's separate iterations remain safe; update the template to iterate the
new paired structure if you choose option (a).
🧹 Nitpick comments (7)
src/main/resources/templates/fragments/cases.html (2)

18-21: Pluralization is English-only and inline.

The count string is built via inline concatenation (' case' + (size == 1 ? '' : 's') + countSuffix). It works, but it's not localizable and forces every caller to pass a countSuffix that already encodes English grammar (e.g. " waiting", " handled"). If you anticipate any i18n or want to dedupe this across other count chips in the app, consider moving the phrasing into messages.properties and using #messages.msg('cases.count', size) (with ICU/{0,choice,...} for plural forms). Non-blocking for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/cases.html` around lines 18 - 21, The
inline English-only pluralization in the span with class "case-count" (which
currently builds the string via ${`#lists.size`(cases) + ' case' +
(`#lists.size`(cases) == 1 ? '' : 's') + countSuffix}) should be moved into
message resources; add a messages.properties key like cases.count that uses
ICU/plural formatting (or choice) and accept the count as a parameter, then
change the template to call `#messages.msg`('cases.count', `#lists.size`(cases)) and
remove the ad-hoc countSuffix usage so localization and pluralization are
centralized.

49-51: Consider using visaStatus.name() for the displayed text too.

Line 50 uses visa.visaStatus.name() for the CSS class while line 51 uses ${visa.visaStatus} for the text, which relies on the enum's toString(). These will diverge if VisaStatus ever overrides toString() (e.g. to return a localized/display label). Aligning both to .name() keeps the rendered text and the status-* class in sync, and matches the PR's stated move toward explicit enum.name() usage in templates.

♻️ Suggested alignment
-                <span class="status"
-                      th:classappend="${visa.visaStatus != null ? 'status-' + `#strings.toLowerCase`(visa.visaStatus.name()) : ''}"
-                      th:text="${visa.visaStatus}">ASSIGNED</span>
+                <span class="status"
+                      th:classappend="${visa.visaStatus != null ? 'status-' + `#strings.toLowerCase`(visa.visaStatus.name()) : ''}"
+                      th:text="${visa.visaStatus != null ? visa.visaStatus.name() : '-'}">ASSIGNED</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/cases.html` around lines 49 - 51, The
displayed status text uses ${visa.visaStatus} while the CSS class uses
visa.visaStatus.name(), which can diverge if VisaStatus overrides toString();
update the template fragment so the span's text also uses visa.visaStatus.name()
(i.e., change the th:text to use ${visa.visaStatus.name()}) to keep the rendered
text and the status-* class in sync with the enum name.
src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java (2)

487-494: Document SYSADMIN visibility expectation for findUnassignedCases.

This method has no handlerId parameter, so any caller that reaches it can see every unassigned SUBMITTED case in the system. Authorization is enforced at the controller (VisaViewController#showCases is the only caller per the provided snippet), but the service exposes a broad listing API. Worth a short Javadoc note that the method is intended for ADMIN/SYSADMIN view contexts only, mirroring the PR's "deliberate SYSADMIN access" documentation effort. No behavior change required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java`
around lines 487 - 494, The public service method
VisaService.findUnassignedCases() returns every unassigned SUBMITTED case and
therefore exposes a broad listing that is intended only for SYSADMIN/ADMIN view
contexts; add a concise Javadoc above the findUnassignedCases method documenting
that this method is deliberately for ADMIN/SYSADMIN visibility only (mirror the
PR's "deliberate SYSADMIN access" wording), note that authorization is enforced
at the controller (VisaViewController#showCases) and callers should not rely on
this service for per-handler-restricted listings—no behavior change required.

469-507: Optional: hoist the repeated Sort to a constant.

Sort.by("updatedAt").descending() is allocated on every call across findOpenCasesByHandler, findUnassignedCases, findHandledCasesByHandler (and several pre-existing methods like findAll, findVisasByApplicant, findVisasByApplicantId, findVisasByHandlerId). Since Sort is immutable, you can hoist it next to OPEN_STATUSES / HANDLED_STATUSES and reuse it. Purely a nit; behavior is unchanged.

♻️ Suggested constant
     private static final List<VisaStatus> HANDLED_STATUSES =
             List.of(VisaStatus.GRANTED, VisaStatus.REJECTED);
+    private static final Sort SORT_BY_UPDATED_AT_DESC =
+            Sort.by("updatedAt").descending();

Then reuse SORT_BY_UPDATED_AT_DESC in the three new case-query methods (and optionally in the existing findAll/findVisasByApplicant/findVisasByHandlerId).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java`
around lines 469 - 507, Multiple methods (findOpenCasesByHandler,
findUnassignedCases, findHandledCasesByHandler) repeatedly allocate the same
Sort instance via Sort.by("updatedAt").descending(); hoist this to a private
static final constant (e.g., SORT_BY_UPDATED_AT_DESC) near
OPEN_STATUSES/HANDLED_STATUSES and replace the inline Sort.by(...) calls in
those methods (and optionally other methods like findAll, findVisasByApplicant,
findVisasByApplicantId, findVisasByHandlerId) to reuse the constant.
src/main/resources/templates/visa/my-applications.html (1)

49-56: Consider explicit .name() and null guards on the #temporals.format calls.

Two minor consistency points:

  1. The PR description calls out a deliberate move toward enum.name() usage in templates for null-safety/consistency. Lines 50–51 still pass visa.visaStatus() directly to #strings.toLowerCase and th:text, relying on Enum#toString. Functionally equivalent for unmodified enums, but using .name() would match the rest of the PR.
  2. #temporals.format(visa.travelDate(), …) and #temporals.format(visa.updatedAt(), …) will throw if either field is null. travelDate is @NotNull on create so it should always be set, but updatedAt depends on whether the row has been persisted/audited. If there's any path where these can be null in the model rendered to this template, a th:if guard or ?: '-' Elvis fallback would be safer.
♻️ Suggested tweaks
-                <td th:text="${visa.visaType()}">STUDENT</td>
+                <td th:text="${visa.visaType().name()}">STUDENT</td>
                 <td>
                     <span class="status"
-                          th:classappend="${'status-' + `#strings.toLowerCase`(visa.visaStatus())}"
-                          th:text="${visa.visaStatus()}">
+                          th:classappend="${'status-' + `#strings.toLowerCase`(visa.visaStatus().name())}"
+                          th:text="${visa.visaStatus().name()}">
                         SUBMITTED
                     </span>
                 </td>
-                <td th:text="${`#temporals.format`(visa.travelDate(), 'yyyy-MM-dd')}">2026-05-20</td>
-                <td th:text="${`#temporals.format`(visa.updatedAt(), 'yyyy-MM-dd HH:mm')}">-</td>
+                <td th:text="${visa.travelDate() != null ? `#temporals.format`(visa.travelDate(), 'yyyy-MM-dd') : '-'}">2026-05-20</td>
+                <td th:text="${visa.updatedAt() != null ? `#temporals.format`(visa.updatedAt(), 'yyyy-MM-dd HH:mm') : '-'}">-</td>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/my-applications.html` around lines 49 - 56,
Use enum.name() instead of relying on toString for the status span by calling
visa.visaStatus().name() when passing into `#strings.toLowerCase` and th:text
(replace uses of visa.visaStatus()); and guard temporal formatting calls by
handling nulls—either wrap `#temporals.format`(visa.travelDate(), ...) /
`#temporals.format`(visa.updatedAt(), ...) with a th:if to only format when
travelDate()/updatedAt() are non-null or use an Elvis fallback (e.g., show '-'
when null) so `#temporals.format` is never invoked with a null argument.
src/main/resources/templates/log/visa.html (1)

58-87: Hide the table when there are no rows.

When logs.totalElements == 0, the <table> still renders its header row and an empty <tbody> immediately above the empty-state message, which produces a visually empty table next to "No log entries match the current filters." Wrapping the table in a th:if (or moving it inside th:if="${logs.totalElements > 0}") avoids the redundant header bar.

♻️ Proposed tweak
-            <table>
+            <table th:if="${logs.totalElements > 0}">
                 <thead>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/log/visa.html` around lines 58 - 87, The table
header is still rendered when there are zero logs; wrap the entire <table> block
(the table, thead and tbody currently iterating with th:each on "log :
${logs.content}") with a Thymeleaf conditional so it only renders when
logs.totalElements > 0 (e.g. add th:if="${logs.totalElements > 0}" on the table
element or move the table inside th:if="${logs.totalElements > 0}"), leaving the
existing <div th:if="${logs.totalElements == 0}" class="empty-state"> message
as-is.
src/main/resources/templates/visa/edit-form.html (1)

174-193: Optional: scope the file-input listener and avoid double-binding.

Two minor robustness items in the script:

  1. document.getElementById('passportFile').addEventListener(...) will throw a TypeError if the element is ever conditionally hidden (e.g., a future template variant). Guarding with ?. or a null check makes this resilient.
  2. If this template is ever included in a larger page or re-rendered, the delegated click handlers attached at module scope can double-bind. Wrapping in DOMContentLoaded or { once: false } semantics is fine today, but consider event delegation on document for better resilience.

Non-blocking; current behavior is correct for the single-page use case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/visa/edit-form.html` around lines 174 - 193,
Guard the passportFile listener and avoid double-binding the delete handlers:
check for the file input element (document.getElementById('passportFile'))
before calling addEventListener (or register the handler inside a
DOMContentLoaded callback) so it won't throw when the element is absent, and
replace the per-button querySelectorAll('.js-delete-doc') loop with a single
delegated click listener on document that uses
event.target.closest('.js-delete-doc') to find the button, reads
btn.dataset.formId and submits the target form after confirm; reference the
IDs/classes 'passportFile', 'file-name' and the class 'js-delete-doc' when
making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/resources/templates/fragments/cases.html`:
- Line 46: The template fragment duplicates the "Unassigned" fallback that
VisaMapper.toDTO already provides; update the template to remove the defensive
ternary and simply render visa.handlerName (e.g., change the td to use
th:text="${visa.handlerName}") or, if you prefer explicitness, reference the
same centralized constant/translation key used by VisaMapper.toDTO so both the
mapper (VisaMapper.toDTO) and the template (fragments/cases.html rendering
visa.handlerName) stay in sync.
- Line 39: The empty action column header (<th></th>) is missing an accessible
name; update the <th> in the cases.html template to include an explicit column
label (e.g., "Actions" or "Details") and mark it visually hidden using your
project's screen-reader utility (or add scope="col" plus the visible/hidden
label). Locate the empty <th> and replace it with a labelled header that uses
your SR-only class or scope attribute so screen readers announce the column for
the row action links.

In `@src/main/resources/templates/visa/edit-form.html`:
- Around line 164-170: The hidden delete forms (th:id="'delete-doc-' +
${iter.index}" with th:action="@{/visa/{id}/documents/delete(id=${visa.id})}")
are missing the CSRF token and will be rejected by Spring Security; add a hidden
input inside each form using Thymeleaf CSRF variables (use _csrf.parameterName
and _csrf.token) so each generated form includes the CSRF parameter and token
before submission.

---

Outside diff comments:
In `@src/main/resources/templates/visa/edit-form.html`:
- Around line 128-143: VisaMapper.toDTO() currently sets downloadUrls to an
empty list which prevents the template check th:if="${not
`#lists.isEmpty`(visa.downloadUrls)}" from ever rendering; change
VisaMapper.toDTO() to populate downloadUrls from the source S3 keys (e.g.,
visa.getS3Keys()) or by generating presigned URLs via your existing S3 helper,
and return those URLs in the DTO instead of List.of(). Also eliminate fragile
parallel iteration by either (a) adding a single documents collection on the DTO
(e.g., List<DocumentDTO> with fields downloadUrl and s3Key) and mapping
visa.getS3Keys() -> List<DocumentDTO>, or (b) ensure downloadUrls and s3Keys are
created in the same order and same length inside VisaMapper.toDTO() so the
template's separate iterations remain safe; update the template to iterate the
new paired structure if you choose option (a).

---

Nitpick comments:
In
`@src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java`:
- Around line 487-494: The public service method
VisaService.findUnassignedCases() returns every unassigned SUBMITTED case and
therefore exposes a broad listing that is intended only for SYSADMIN/ADMIN view
contexts; add a concise Javadoc above the findUnassignedCases method documenting
that this method is deliberately for ADMIN/SYSADMIN visibility only (mirror the
PR's "deliberate SYSADMIN access" wording), note that authorization is enforced
at the controller (VisaViewController#showCases) and callers should not rely on
this service for per-handler-restricted listings—no behavior change required.
- Around line 469-507: Multiple methods (findOpenCasesByHandler,
findUnassignedCases, findHandledCasesByHandler) repeatedly allocate the same
Sort instance via Sort.by("updatedAt").descending(); hoist this to a private
static final constant (e.g., SORT_BY_UPDATED_AT_DESC) near
OPEN_STATUSES/HANDLED_STATUSES and replace the inline Sort.by(...) calls in
those methods (and optionally other methods like findAll, findVisasByApplicant,
findVisasByApplicantId, findVisasByHandlerId) to reuse the constant.

In `@src/main/resources/templates/fragments/cases.html`:
- Around line 18-21: The inline English-only pluralization in the span with
class "case-count" (which currently builds the string via ${`#lists.size`(cases) +
' case' + (`#lists.size`(cases) == 1 ? '' : 's') + countSuffix}) should be moved
into message resources; add a messages.properties key like cases.count that uses
ICU/plural formatting (or choice) and accept the count as a parameter, then
change the template to call `#messages.msg`('cases.count', `#lists.size`(cases)) and
remove the ad-hoc countSuffix usage so localization and pluralization are
centralized.
- Around line 49-51: The displayed status text uses ${visa.visaStatus} while the
CSS class uses visa.visaStatus.name(), which can diverge if VisaStatus overrides
toString(); update the template fragment so the span's text also uses
visa.visaStatus.name() (i.e., change the th:text to use
${visa.visaStatus.name()}) to keep the rendered text and the status-* class in
sync with the enum name.

In `@src/main/resources/templates/log/visa.html`:
- Around line 58-87: The table header is still rendered when there are zero
logs; wrap the entire <table> block (the table, thead and tbody currently
iterating with th:each on "log : ${logs.content}") with a Thymeleaf conditional
so it only renders when logs.totalElements > 0 (e.g. add
th:if="${logs.totalElements > 0}" on the table element or move the table inside
th:if="${logs.totalElements > 0}"), leaving the existing <div
th:if="${logs.totalElements == 0}" class="empty-state"> message as-is.

In `@src/main/resources/templates/visa/edit-form.html`:
- Around line 174-193: Guard the passportFile listener and avoid double-binding
the delete handlers: check for the file input element
(document.getElementById('passportFile')) before calling addEventListener (or
register the handler inside a DOMContentLoaded callback) so it won't throw when
the element is absent, and replace the per-button
querySelectorAll('.js-delete-doc') loop with a single delegated click listener
on document that uses event.target.closest('.js-delete-doc') to find the button,
reads btn.dataset.formId and submits the target form after confirm; reference
the IDs/classes 'passportFile', 'file-name' and the class 'js-delete-doc' when
making these changes.

In `@src/main/resources/templates/visa/my-applications.html`:
- Around line 49-56: Use enum.name() instead of relying on toString for the
status span by calling visa.visaStatus().name() when passing into
`#strings.toLowerCase` and th:text (replace uses of visa.visaStatus()); and guard
temporal formatting calls by handling nulls—either wrap
`#temporals.format`(visa.travelDate(), ...) / `#temporals.format`(visa.updatedAt(),
...) with a th:if to only format when travelDate()/updatedAt() are non-null or
use an Elvis fallback (e.g., show '-' when null) so `#temporals.format` is never
invoked with a null argument.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 06a8e76c-2290-470a-b8d1-b270a479f882

📥 Commits

Reviewing files that changed from the base of the PR and between db4bf3b and 21ad19a.

📒 Files selected for processing (20)
  • src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java
  • src/main/java/org/example/visacasemanagementsystem/audit/controller/LogViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/dto/CreateVisaDTO.java
  • src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java
  • src/main/resources/static/css/components.css
  • src/main/resources/static/css/tokens.css
  • src/main/resources/templates/error/error.html
  • src/main/resources/templates/fragments/cases.html
  • src/main/resources/templates/fragments/header.html
  • src/main/resources/templates/log/user.html
  • src/main/resources/templates/log/visa.html
  • src/main/resources/templates/user/list.html
  • src/main/resources/templates/visa/apply-form.html
  • src/main/resources/templates/visa/edit-form.html
  • src/main/resources/templates/visa/my-applications.html
  • src/test/java/org/example/visacasemanagementsystem/audit/service/VisaLogServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaMapperTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java
✅ Files skipped from review due to trivial changes (2)
  • src/main/resources/static/css/components.css
  • src/main/resources/static/css/tokens.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/resources/templates/log/user.html
  • src/main/resources/templates/visa/apply-form.html

Comment thread src/main/resources/templates/fragments/cases.html Outdated
Comment thread src/main/resources/templates/fragments/cases.html Outdated
Comment thread src/main/resources/templates/visa/edit-form.html
- Add CSRF token to hidden delete forms in edit-form.html
- Added accessible name to <th> and handlerName fallback to "Unassigned" in cases.html
…i-design

# Conflicts:
#	src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java
#	src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java
#	src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java
#	src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java
#	src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java
#	src/main/resources/templates/user/login.html
#	src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java
#	src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java
#	src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java
- Solved merge issues
- Solved merge issues

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java (1)

19-19: ⚠️ Potential issue | 🟡 Minor

Unused withDefaults static import — Spotless failure.

After replacing logout(withDefaults()) with logout(AbstractHttpConfigurer::disable), the static import on line 19 has no remaining call sites and is what spotless:check is reporting on line 16. Drop it.

🧹 Proposed fix
-import static org.springframework.security.config.Customizer.withDefaults;
-
 `@Configuration`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`
at line 19, Remove the now-unused static import withDefaults from
SecurityConfig.java — locate the import statement "import static
org.springframework.security.config.Customizer.withDefaults;" and delete it
since you already replaced logout(withDefaults()) with
logout(AbstractHttpConfigurer::disable) and there are no remaining references to
withDefaults.
🧹 Nitpick comments (2)
src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java (1)

350-359: Redundant @WithMockUser next to .with(authentication(...)).

SecurityMockMvcRequestPostProcessors.authentication(...) already overrides the SecurityContext for this request, so the class-level @WithMockUser setup is shadowed. Either drop @WithMockUser or remove the explicit authentication(...) to avoid future confusion about which principal is actually under test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java`
around lines 350 - 359, The test
userListView_AsNonSysAdmin_ShouldReturnForbidden has a redundant `@WithMockUser`
which is shadowed by the explicit .with(authentication(authFor(...))) request
post-processor; remove one of them to avoid ambiguity — either delete the
`@WithMockUser` annotation or stop using .with(authentication(...)) in the
mockMvc.perform call so the test's principal is set from a single source (refer
to the authFor(...) helper and the mockMvc.perform(get("/user/list")...)
invocation when making the change).
src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java (1)

155-167: Stale currentUserId request parameter — controller no longer reads it.

submitApplication, processUpdate, etc. now resolve the user from @AuthenticationPrincipal UserPrincipal principal; the param("currentUserId", ...) calls on lines 159, 182, 252, 279, 310, and 343 are leftovers from the pre-refactor signature. They're silently ignored, but they obscure intent and will mislead the next person reading the test. Consider removing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`
around lines 155 - 167, Tests are passing a stale request parameter
"currentUserId" which the controller no longer reads (submitApplication,
processUpdate, etc. now use `@AuthenticationPrincipal` UserPrincipal); remove all
.param("currentUserId", ...) calls from VisaViewControllerTest and instead
ensure the MockMvc request supplies an authenticated principal (e.g., use
MockMvc security helpers like with(user(...)) or with(authentication(...)) or
the test helper that builds a UserPrincipal) so the controller receives the
expected principal; update any related test helper setup that previously relied
on the param to populate authentication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java`:
- Around line 47-50: Method-level authorization is missing on
UserLogService.findFiltered(...) — add a `@PreAuthorize` annotation on the
findFiltered method (in the UserLogService class) to mirror the existing
protection on findAll() (e.g., `@PreAuthorize`("isAuthenticated()") or stricter
`@PreAuthorize`("hasRole('SYSADMIN')") to match the URL rule) so service-level
callers cannot bypass access checks; place the annotation directly above the
findFiltered(...) declaration and ensure the class imports the correct Spring
Security annotation.

In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`:
- Around line 53-54: The catch-all rule in SecurityConfig that uses
.anyRequest().hasRole("SYSADMIN") is too broad and blocks special paths; update
the HttpSecurity config (the chain where .requestMatchers(...) and
.anyRequest().hasRole(...) are defined) to explicitly permit /favicon.ico,
/login/oauth2/** and /error by inserting
.requestMatchers("/favicon.ico").permitAll(),
.requestMatchers("/login/oauth2/**").permitAll(), and
.requestMatchers("/error").permitAll() before the .anyRequest() rule (or
alternatively change .anyRequest().hasRole("SYSADMIN") to
.anyRequest().denyAll() for explicit deny semantics). Ensure these
requestMatchers are added in the same configuration method in SecurityConfig so
they execute prior to the catch‑all.
- Line 67: The logout path currently only clears the SecurityContext (via
SecurityContextLogoutHandler) but does not invalidate the HTTP session, leaving
session cookies valid; update the custom logout handler (the method that calls
new SecurityContextLogoutHandler().logout(...), e.g. your controller logout
method and any logout configuration referenced in SecurityConfig where
.logout(...) is configured) to call request.getSession(false) and if non-null
call invalidate() after invoking SecurityContextLogoutHandler.logout; ensure the
invalidation happens before returning the redirect so the server-side session is
destroyed and the session cookie cannot be reused.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`:
- Line 17: The import org.springframework.security.access.prepost.PreAuthorize
in VisaViewController is unused and causing Spotless failure; fix by either
removing that unused import line from the VisaViewController class or re-enable
the commented `@PreAuthorize` annotations on the relevant controller methods so
the import is used (ensure any re-enabled `@PreAuthorize` uses the correct SpEL
and required roles/permissions).
- Around line 47-80: The controller methods showMyApplications, showApplyForm,
submitApplication and showCases currently have their `@PreAuthorize` annotations
commented out, allowing any authenticated user to access endpoints that should
be role-restricted; restore method-level security by uncommenting and applying
the intended checks: use `@PreAuthorize`("hasRole('USER')") on showApplyForm and
submitApplication (and showMyApplications if meant for USERs) and
`@PreAuthorize`("hasAnyRole('ADMIN','SYSADMIN')") on showCases so only
admins/sysadmins can see unassigned cases, then rebuild and run tests to verify
access control; locate these annotations adjacent to the method declarations in
VisaViewController to make the change.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`:
- Around line 73-97: The test method
showCases_AsAdmin_ShouldReturnThreeListsAndCasesView currently uses
`@WithMockUser` which defaults to ROLE_USER and therefore doesn't match the
intended admin principal; update the test annotation to `@WithMockUser`(roles =
"ADMIN") (and similarly adjust other tests that expect admin/SYSADMIN) so
MockMvc's security context matches the createMockUser(UserAuthorization.ADMIN)
setup and the controller method showCases (protected by `@PreAuthorize`) will be
authorized during the test.

---

Outside diff comments:
In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`:
- Line 19: Remove the now-unused static import withDefaults from
SecurityConfig.java — locate the import statement "import static
org.springframework.security.config.Customizer.withDefaults;" and delete it
since you already replaced logout(withDefaults()) with
logout(AbstractHttpConfigurer::disable) and there are no remaining references to
withDefaults.

---

Nitpick comments:
In
`@src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java`:
- Around line 350-359: The test userListView_AsNonSysAdmin_ShouldReturnForbidden
has a redundant `@WithMockUser` which is shadowed by the explicit
.with(authentication(authFor(...))) request post-processor; remove one of them
to avoid ambiguity — either delete the `@WithMockUser` annotation or stop using
.with(authentication(...)) in the mockMvc.perform call so the test's principal
is set from a single source (refer to the authFor(...) helper and the
mockMvc.perform(get("/user/list")...) invocation when making the change).

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`:
- Around line 155-167: Tests are passing a stale request parameter
"currentUserId" which the controller no longer reads (submitApplication,
processUpdate, etc. now use `@AuthenticationPrincipal` UserPrincipal); remove all
.param("currentUserId", ...) calls from VisaViewControllerTest and instead
ensure the MockMvc request supplies an authenticated principal (e.g., use
MockMvc security helpers like with(user(...)) or with(authentication(...)) or
the test helper that builds a UserPrincipal) so the controller receives the
expected principal; update any related test helper setup that previously relied
on the param to populate authentication.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d59ee1a-d1ff-4d0d-afba-229479827011

📥 Commits

Reviewing files that changed from the base of the PR and between 21ad19a and 322d6e1.

📒 Files selected for processing (13)
  • src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java
  • src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java
  • src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java
  • src/main/resources/templates/fragments/cases.html
  • src/main/resources/templates/user/login.html
  • src/main/resources/templates/visa/edit-form.html
  • src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java
✅ Files skipped from review due to trivial changes (3)
  • src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java
  • src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java
  • src/main/resources/templates/visa/edit-form.html
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/resources/templates/user/login.html
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java
  • src/main/resources/templates/fragments/cases.html
  • src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java

Comment on lines +47 to +80
// @PreAuthorize("hasRole('USER')")
@GetMapping("/my-applications")
public String showMyApplications(@AuthenticationPrincipal UserPrincipal principal, Model model) {
UserDTO user = userService.findById(principal.getUserId())
.orElseThrow(() -> new EntityNotFoundException("User not found."));
List<VisaDTO> visas;
// If the current user = ADMIN/SYSADMIN show everything
if ( user.userAuthorization() == UserAuthorization.ADMIN ||
user.userAuthorization() == UserAuthorization.SYSADMIN) {
visas = visaService.findAll();
} else {
// Normal user/applicant can only se their own visa applications
visas = visaService.findVisasByApplicant(principal.getUserId());
}

// Send data to Thymeleaf
List<VisaDTO> visas = visaService.findVisasByApplicant(principal.getUserId());

model.addAttribute("visas", visas);
model.addAttribute("currentUser", user);

return "visa/dashboard";
return "visa/my-applications";
}

// ─── ADMIN + SYSADMIN: "Visa Cases" three-list page ────────────────────
// @PreAuthorize("hasAnyRole('ADMIN', 'SYSADMIN')")
@GetMapping("/cases")
public String showCases(@AuthenticationPrincipal UserPrincipal principal, Model model) {
UserDTO user = userService.findById(principal.getUserId())
.orElseThrow(() -> new EntityNotFoundException("User not found."));

Long meId = principal.getUserId();

List<VisaDTO> openCases = visaService.findOpenCasesByHandler(meId);
List<VisaDTO> unassignedCases = visaService.findUnassignedCases();
List<VisaDTO> handledCases = visaService.findHandledCasesByHandler(meId);

model.addAttribute("currentUser", user);
model.addAttribute("openCases", openCases);
model.addAttribute("unassignedCases", unassignedCases);
model.addAttribute("handledCases", handledCases);

return "visa/cases";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=java -C2 '/visa/cases|hasAnyRole\(.*ADMIN.*SYSADMIN'
rg -nP --type=java -C2 '@PreAuthorize' src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java

Repository: ithsjava25/visa-case-management-system

Length of output: 8853


🏁 Script executed:

#!/bin/bash
# Check SecurityConfig for URL-level role protection
fd -t f SecurityConfig.java | head -1 | xargs cat -n

# Check PreAuthorize import in VisaViewController
rg -n 'import.*PreAuthorize|import.*security.access' src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java

Repository: ithsjava25/visa-case-management-system

Length of output: 4994


🏁 Script executed:

#!/bin/bash
# Get the import section of VisaViewController
head -n 30 src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java

Repository: ithsjava25/visa-case-management-system

Length of output: 1466


🏁 Script executed:

#!/bin/bash
# Check if PreAuthorize import exists anywhere in the file
rg 'PreAuthorize' src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java | head -20

Repository: ithsjava25/visa-case-management-system

Length of output: 215


🏁 Script executed:

#!/bin/bash
# Check if there's any mention of PreAuthorize import earlier in file or in comments
rg -n 'import.*PreAuthorize|PreAuthorize' src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java | head -5

# Double-check the entire import block for any security imports
head -n 25 src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java | rg 'import'

Repository: ithsjava25/visa-case-management-system

Length of output: 1470


Authorization bypass: /visa/cases, /visa/apply, and /visa/my-applications lack role enforcement.

SecurityConfig protects /visa/** with only .authenticated() (line 47), with no URL-level role gating. The method-level @PreAuthorize annotations on showCases, showApplyForm, submitApplication, and showMyApplications are commented out (lines 46, 61, 82) and provide no protection.

Result: any authenticated USER can:

  • GET /visa/cases and access findUnassignedCases(), leaking the global unassigned-case list to regular applicants.
  • GET /visa/apply and submit applications without role restriction (contradicts the "USER only" intent).
  • GET /visa/my-applications is accessible to ADMIN/SYSADMIN (less severe but violates documented role separation).

Uncomment the @PreAuthorize annotations to restore role-based method-level protection:

Re-enable method-level authorization
-    // ─── USER: "My Applications" landing page ─────────────────────────────
-//    `@PreAuthorize`("hasRole('USER')")
+    // ─── USER: "My Applications" landing page ─────────────────────────────
+    `@PreAuthorize`("hasRole('USER')")
     `@GetMapping`("/my-applications")
@@
-    // ─── ADMIN + SYSADMIN: "Visa Cases" three-list page ────────────────────
-//    `@PreAuthorize`("hasAnyRole('ADMIN', 'SYSADMIN')")
+    // ─── ADMIN + SYSADMIN: "Visa Cases" three-list page ────────────────────
+    `@PreAuthorize`("hasAnyRole('ADMIN', 'SYSADMIN')")
     `@GetMapping`("/cases")
@@
-    // ─── Apply / edit / details — unchanged semantics, renamed URLs ────────
-//    `@PreAuthorize`("hasRole('USER')")
+    // ─── Apply / edit / details — unchanged semantics, renamed URLs ────────
+    `@PreAuthorize`("hasRole('USER')")
     `@GetMapping`("/apply")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`
around lines 47 - 80, The controller methods showMyApplications, showApplyForm,
submitApplication and showCases currently have their `@PreAuthorize` annotations
commented out, allowing any authenticated user to access endpoints that should
be role-restricted; restore method-level security by uncommenting and applying
the intended checks: use `@PreAuthorize`("hasRole('USER')") on showApplyForm and
submitApplication (and showMyApplications if meant for USERs) and
`@PreAuthorize`("hasAnyRole('ADMIN','SYSADMIN')") on showCases so only
admins/sysadmins can see unassigned cases, then rebuild and run tests to verify
access control; locate these annotations adjacent to the method declarations in
VisaViewController to make the change.

- Solved code rabbit suggestions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java (1)

58-73: ⚠️ Potential issue | 🟡 Minor

POST /user/signup is reachable by already-authenticated users.

The GET handler now redirects authenticated users to /home (line 49-51), but the POST counterpart will happily create another USER account for any logged-in caller (and even one with ADMIN/SYSADMIN role, since SecurityConfig keeps /user/signup permitAll). Consider mirroring the GET-side guard so authenticated principals are bounced before userService.createUser runs:

🛡️ Suggested guard
 `@PostMapping`("/user/signup")
-public String createUser(`@RequestParam` String fullName,
+public String createUser(`@AuthenticationPrincipal` UserPrincipal principal,
+                         `@RequestParam` String fullName,
                          `@RequestParam` String email,
                          `@RequestParam` String password,
                          Model model) {
+    if (principal != null) {
+        return "redirect:/home";
+    }
     try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java`
around lines 58 - 73, The POST createUser handler allows authenticated
principals to create new accounts; add the same guard as the GET signup handler
at the start of the createUser method so authenticated users are redirected to
"/home" before calling userService.createUser. Implement this by checking the
current Authentication (e.g.,
SecurityContextHolder.getContext().getAuthentication() or an injected
Principal/@AuthenticationPrincipal), verify authentication.isAuthenticated() and
that the principal is not anonymous, and return "redirect:/home" if true;
otherwise proceed to construct CreateUserDTO and call userService.createUser as
before.
src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java (1)

358-437: ⚠️ Potential issue | 🟡 Minor

Admin-only handler tests still default to ROLE_USER via plain @WithMockUser.

showCases_AsAdmin was correctly updated to @WithMockUser(roles = "ADMIN"), but approveVisa_AsAdmin (Line 359), assignCaseToHandler_Success (Line 380), requestMoreInformation_Success (Line 397), rejectVisa_AsAdmin (Line 418), and viewDetails_AsAdmin_ShouldAllowViewingOtherVisas (Line 491) are still using plain @WithMockUser, which produces a ROLE_USER principal. They currently pass only because @WebMvcTest doesn't load SecurityConfig's URL-level rules. Once method-level @PreAuthorize is added (see the critical SecurityConfig comment), these tests will start returning 403 and silently fail to validate the admin path.

🧪 Suggested annotation alignment
-    `@WithMockUser`
+    `@WithMockUser`(roles = "ADMIN")
     void approveVisa_AsAdmin_ShouldRedirectToDetailsView() throws Exception {
…
-    `@WithMockUser`
+    `@WithMockUser`(roles = "ADMIN")
     void assignCaseToHandler_Success_ShouldRedirectToDetails() throws Exception {
…
-    `@WithMockUser`
+    `@WithMockUser`(roles = "ADMIN")
     void requestMoreInformation_Success_ShouldRedirectToDetails() throws Exception {
…
-    `@WithMockUser`
+    `@WithMockUser`(roles = "ADMIN")
     void rejectVisa_AsAdmin_ShouldRedirectToDetailsView() throws Exception {
…
-    `@WithMockUser`
+    `@WithMockUser`(roles = "ADMIN")
     void viewDetails_AsAdmin_ShouldAllowViewingOtherVisas() throws Exception {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`
around lines 358 - 437, The admin-only tests
(approveVisa_AsAdmin_ShouldRedirectToDetailsView,
assignCaseToHandler_Success_ShouldRedirectToDetails,
requestMoreInformation_Success_ShouldRedirectToDetails,
rejectVisa_AsAdmin_ShouldRedirectToDetailsView and the later
viewDetails_AsAdmin_ShouldAllowViewingOtherVisas) are using plain `@WithMockUser`
which creates ROLE_USER; update each of these test methods to use
`@WithMockUser`(roles = "ADMIN") so the mock principal has ADMIN authority and the
controller’s admin-only checks (e.g., `@PreAuthorize`) will pass; locate the
annotations above the methods named
approveVisa_AsAdmin_ShouldRedirectToDetailsView,
assignCaseToHandler_Success_ShouldRedirectToDetails,
requestMoreInformation_Success_ShouldRedirectToDetails,
rejectVisa_AsAdmin_ShouldRedirectToDetailsView and
viewDetails_AsAdmin_ShouldAllowViewingOtherVisas and change the annotation
accordingly.
src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java (1)

134-258: ⚠️ Potential issue | 🔴 Critical

All /visa/{id}* endpoints are SYSADMIN-only — USERs and ADMINs cannot access them.

SecurityConfig contains no explicit matchers for /visa/{id}, /visa/{id}/edit, /visa/{id}/approve, /visa/{id}/reject, /visa/{id}/request-info, /visa/{id}/assign, /visa/{id}/documents/delete, or the parameterized routes. These paths fall through to line 58: .anyRequest().hasRole("SYSADMIN"). Meanwhile, VisaViewController has no method-level @PreAuthorize annotations to override this catch-all.

Consequence at runtime:

  • A USER cannot GET /visa/{id} (view own application) — SecurityFilterChain returns 403 before the controller is invoked.
  • An ADMIN cannot POST /visa/{id}/approve, /reject, /request-info, or /assign — same 403.
  • Even though VisaService has @PreAuthorize("hasRole('ADMIN')") on approveVisa() etc., those service-level guards are never reached because URL-level authorization is enforced first.

Fix: Either add a /visa/** matcher allowing all authenticated users (lines 42–49), or annotate each controller method with @PreAuthorize reflecting the intended role (@PreAuthorize("hasAnyRole('USER', 'ADMIN', 'SYSADMIN')") for view/edit, @PreAuthorize("hasAnyRole('ADMIN', 'SYSADMIN')") for case actions). The comment in lines 42–43 ("role-specific protection lives on individual @PreAuthorize") does not apply here since those annotations are absent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`
around lines 134 - 258, The controller endpoints under VisaViewController (e.g.,
showEditForm, processUpdate, deleteVisaDocument, approveVisa,
requestMoreInformation, rejectVisa, assignCaseToHandler) are blocked by the
global .anyRequest().hasRole("SYSADMIN") rule; add explicit method-level
security annotations to restore intended access: annotate view/edit endpoints
(showEditForm, processUpdate, and the GET /visa/{id} handler) with
`@PreAuthorize`("hasAnyRole('USER','ADMIN','SYSADMIN')") and annotate case-action
endpoints (approveVisa, rejectVisa, requestMoreInformation, assignCaseToHandler,
deleteVisaDocument if appropriate) with
`@PreAuthorize`("hasAnyRole('ADMIN','SYSADMIN')"); alternatively, adjust
SecurityConfig to include a /visa/** matcher that permits authenticated users
and rely on service-layer `@PreAuthorize` where present—pick one approach and
apply consistently to the methods named above.
🧹 Nitpick comments (5)
src/main/resources/templates/fragments/cases.html (2)

18-22: Count expression concatenates null if countSuffix is omitted.

If a caller invokes caseSection(...) without an explicit countSuffix (or passes null), the rendered label becomes e.g. "3 casesnull". Current callers in visa/cases.html reportedly pass strings, but the fragment is more robust if it defaults explicitly:

♻️ Suggested null-safe rendering
-            <span class="case-count"
-                  th:text="${`#lists.size`(cases) + ' case' + (`#lists.size`(cases) == 1 ? '' : 's') + countSuffix}">
+            <span class="case-count"
+                  th:text="${`#lists.size`(cases) + ' case' + (`#lists.size`(cases) == 1 ? '' : 's') + (countSuffix != null ? countSuffix : '')}">
                 0 cases
             </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/cases.html` around lines 18 - 22, The
count expression currently concatenates a null countSuffix and results like "3
casesnull"; update the span's Thymeleaf expression to default countSuffix to an
empty string when null (e.g., use the Elvis/default operator or
`#strings.defaultString`) so the computed text for the element (the expression in
the span with class "case-count") appends '' instead of null; keep the rest of
the pluralization logic intact.

46-50: Enum rendering uses default toString() — confirm consistency with the rest of the PR.

Line 46 (${visa.visaType}) and line 50 (${visa.visaStatus}) rely on enum default toString(), while line 49 explicitly calls visa.visaStatus.name(). The PR objective mentions "use enum.name() in templates" for stability against future toString() overrides. If VisaType or VisaStatus ever gain a custom toString() (e.g., for a localized label), the text on lines 46/50 will silently diverge from the CSS class on line 49.

♻️ Optional consistency tweak
-            <td th:text="${visa.visaType}">STUDENT</td>
+            <td th:text="${visa.visaType != null ? visa.visaType.name() : ''}">STUDENT</td>
             <td>
                 <span class="status"
                       th:classappend="${visa.visaStatus != null ? 'status-' + `#strings.toLowerCase`(visa.visaStatus.name()) : ''}"
-                      th:text="${visa.visaStatus}">ASSIGNED</span>
+                      th:text="${visa.visaStatus != null ? visa.visaStatus.name() : ''}">ASSIGNED</span>
             </td>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/cases.html` around lines 46 - 50, The
template currently renders enums via their default toString() at
${visa.visaType} and ${visa.visaStatus} while using visa.visaStatus.name() for
the CSS class; update the text renderings to use enum.name() as well (e.g.,
replace references to visa.visaType and visa.visaStatus with
visa.visaType.name() and visa.visaStatus.name()) so the displayed text and the
class generation (which uses `#strings.toLowerCase`(visa.visaStatus.name()))
remain consistent even if toString() is later overridden.
src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java (1)

322-459: LGTM — solid coverage for the new case-list queries.

The new tests correctly assert both the repository call signatures (status sets + Sort.by("updatedAt").descending()) and the DTO mapping/order. Empty-result paths verifying verifyNoInteractions(visaMapper) is a nice touch.

Optional nit: in the empty-list tests (Lines 362-375, 401-413, 446-459) the stubs use anyLong()/anyList()/any(Sort.class), so an accidentally wrong status set or sort wouldn't be caught. Tightening these to the same eq(...) arguments as their non-empty counterparts would close the gap.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java`
around lines 322 - 459, The empty-list tests use loose matchers which can hide
incorrect query args; update the stubs in
findOpenCasesByHandler_shouldReturnEmptyList_whenHandlerHasNoOpenCases to use
eq(handlerId), eq(List.of(VisaStatus.ASSIGNED, VisaStatus.INCOMPLETE)),
eq(Sort.by("updatedAt").descending()) for the
visaRepository.findByHandler_IdAndVisaStatusIn call, update
findUnassignedCases_shouldReturnEmptyList_whenNoSubmittedCasesExist to use
eq(VisaStatus.SUBMITTED) and eq(Sort.by("updatedAt").descending()) for
visaRepository.findByVisaStatusAndHandlerIsNull, and update
findHandledCasesByHandler_shouldReturnEmptyList_whenHandlerHasClosedNothing to
use eq(handlerId), eq(List.of(VisaStatus.GRANTED, VisaStatus.REJECTED)),
eq(Sort.by("updatedAt").descending()) for
visaRepository.findByHandler_IdAndVisaStatusIn so the stubs exactly match the
expected query arguments and prevent false positives.
src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java (1)

157-166: Stale .param("currentUserId", …) form fields.

currentUserId is no longer a @RequestParam on the controller (it's resolved from @AuthenticationPrincipal), so these params are silently ignored. They aren't harmful, just dead code that suggests the old API. Cleaning them up makes the tests easier to read and avoids the impression that the controller still binds the parameter.

Also applies to: 181-187, 249-258, 277-285

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`
around lines 157 - 166, Tests in VisaViewControllerTest are passing a stale form
param "currentUserId" to multipart requests even though the controller now
resolves the user via `@AuthenticationPrincipal`, so remove the dead
.param("currentUserId", userId.toString()) entries from the multipart requests
in the test methods (e.g., the mockMvc.perform(multipart("/visa/apply")...) and
the other occurrences called out) to clean up the tests and avoid implying the
controller binds that request parameter; keep the remaining params and csrf()
as-is.
src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java (1)

94-106: Remove redundant session invalidation — SecurityContextLogoutHandler already handles it.

SecurityContextLogoutHandler.logout() invalidates the HTTP session by default (invalidateHttpSession=true). The explicit getSession(false)/invalidate() block on lines 101–104 is redundant and can be removed. While harmless on Tomcat (where getSession(false) returns null after invalidation), other containers may retain the session reference and throw IllegalStateException on a second invalidate() call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java`
around lines 94 - 106, The logout method in UserViewController currently calls
SecurityContextLogoutHandler.logout(request, response, auth) and then
redundantly fetches and invalidates the HttpSession again; remove the explicit
session invalidation block (the getSession(false)/invalidate() lines) to avoid
double-invalidation and potential IllegalStateException in some containers. Keep
the SecurityContextLogoutHandler.logout(...) call as the sole logout/session
invalidation mechanism (ensure any custom logout handler configuration still
uses invalidateHttpSession=true if needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`:
- Around line 42-58: SecurityConfig currently only matches /visa/cases,
/visa/my-applications and /visa/apply so per-case endpoints like GET /visa/{id},
/visa/{id}/edit and all POST /visa/{id}/... fall through to
.anyRequest().hasRole("SYSADMIN") and become inaccessible; update the
SecurityConfig requestMatchers to include a more general /visa/** rule (e.g.,
add a .requestMatchers("/visa/**").authenticated() or finer-grained roles as
needed) and then add explicit method-level `@PreAuthorize` annotations on
VisaViewController methods (and keep/adjust VisaService `@PreAuthorize`
annotations) so owner-facing actions use hasRole('USER') while admin handlers
use hasAnyRole('ADMIN','SYSADMIN').

---

Outside diff comments:
In
`@src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java`:
- Around line 58-73: The POST createUser handler allows authenticated principals
to create new accounts; add the same guard as the GET signup handler at the
start of the createUser method so authenticated users are redirected to "/home"
before calling userService.createUser. Implement this by checking the current
Authentication (e.g., SecurityContextHolder.getContext().getAuthentication() or
an injected Principal/@AuthenticationPrincipal), verify
authentication.isAuthenticated() and that the principal is not anonymous, and
return "redirect:/home" if true; otherwise proceed to construct CreateUserDTO
and call userService.createUser as before.

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`:
- Around line 134-258: The controller endpoints under VisaViewController (e.g.,
showEditForm, processUpdate, deleteVisaDocument, approveVisa,
requestMoreInformation, rejectVisa, assignCaseToHandler) are blocked by the
global .anyRequest().hasRole("SYSADMIN") rule; add explicit method-level
security annotations to restore intended access: annotate view/edit endpoints
(showEditForm, processUpdate, and the GET /visa/{id} handler) with
`@PreAuthorize`("hasAnyRole('USER','ADMIN','SYSADMIN')") and annotate case-action
endpoints (approveVisa, rejectVisa, requestMoreInformation, assignCaseToHandler,
deleteVisaDocument if appropriate) with
`@PreAuthorize`("hasAnyRole('ADMIN','SYSADMIN')"); alternatively, adjust
SecurityConfig to include a /visa/** matcher that permits authenticated users
and rely on service-layer `@PreAuthorize` where present—pick one approach and
apply consistently to the methods named above.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`:
- Around line 358-437: The admin-only tests
(approveVisa_AsAdmin_ShouldRedirectToDetailsView,
assignCaseToHandler_Success_ShouldRedirectToDetails,
requestMoreInformation_Success_ShouldRedirectToDetails,
rejectVisa_AsAdmin_ShouldRedirectToDetailsView and the later
viewDetails_AsAdmin_ShouldAllowViewingOtherVisas) are using plain `@WithMockUser`
which creates ROLE_USER; update each of these test methods to use
`@WithMockUser`(roles = "ADMIN") so the mock principal has ADMIN authority and the
controller’s admin-only checks (e.g., `@PreAuthorize`) will pass; locate the
annotations above the methods named
approveVisa_AsAdmin_ShouldRedirectToDetailsView,
assignCaseToHandler_Success_ShouldRedirectToDetails,
requestMoreInformation_Success_ShouldRedirectToDetails,
rejectVisa_AsAdmin_ShouldRedirectToDetailsView and
viewDetails_AsAdmin_ShouldAllowViewingOtherVisas and change the annotation
accordingly.

---

Nitpick comments:
In
`@src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java`:
- Around line 94-106: The logout method in UserViewController currently calls
SecurityContextLogoutHandler.logout(request, response, auth) and then
redundantly fetches and invalidates the HttpSession again; remove the explicit
session invalidation block (the getSession(false)/invalidate() lines) to avoid
double-invalidation and potential IllegalStateException in some containers. Keep
the SecurityContextLogoutHandler.logout(...) call as the sole logout/session
invalidation mechanism (ensure any custom logout handler configuration still
uses invalidateHttpSession=true if needed).

In `@src/main/resources/templates/fragments/cases.html`:
- Around line 18-22: The count expression currently concatenates a null
countSuffix and results like "3 casesnull"; update the span's Thymeleaf
expression to default countSuffix to an empty string when null (e.g., use the
Elvis/default operator or `#strings.defaultString`) so the computed text for the
element (the expression in the span with class "case-count") appends '' instead
of null; keep the rest of the pluralization logic intact.
- Around line 46-50: The template currently renders enums via their default
toString() at ${visa.visaType} and ${visa.visaStatus} while using
visa.visaStatus.name() for the CSS class; update the text renderings to use
enum.name() as well (e.g., replace references to visa.visaType and
visa.visaStatus with visa.visaType.name() and visa.visaStatus.name()) so the
displayed text and the class generation (which uses
`#strings.toLowerCase`(visa.visaStatus.name())) remain consistent even if
toString() is later overridden.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java`:
- Around line 322-459: The empty-list tests use loose matchers which can hide
incorrect query args; update the stubs in
findOpenCasesByHandler_shouldReturnEmptyList_whenHandlerHasNoOpenCases to use
eq(handlerId), eq(List.of(VisaStatus.ASSIGNED, VisaStatus.INCOMPLETE)),
eq(Sort.by("updatedAt").descending()) for the
visaRepository.findByHandler_IdAndVisaStatusIn call, update
findUnassignedCases_shouldReturnEmptyList_whenNoSubmittedCasesExist to use
eq(VisaStatus.SUBMITTED) and eq(Sort.by("updatedAt").descending()) for
visaRepository.findByVisaStatusAndHandlerIsNull, and update
findHandledCasesByHandler_shouldReturnEmptyList_whenHandlerHasClosedNothing to
use eq(handlerId), eq(List.of(VisaStatus.GRANTED, VisaStatus.REJECTED)),
eq(Sort.by("updatedAt").descending()) for
visaRepository.findByHandler_IdAndVisaStatusIn so the stubs exactly match the
expected query arguments and prevent false positives.

In
`@src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java`:
- Around line 157-166: Tests in VisaViewControllerTest are passing a stale form
param "currentUserId" to multipart requests even though the controller now
resolves the user via `@AuthenticationPrincipal`, so remove the dead
.param("currentUserId", userId.toString()) entries from the multipart requests
in the test methods (e.g., the mockMvc.perform(multipart("/visa/apply")...) and
the other occurrences called out) to clean up the tests and avoid implying the
controller binds that request parameter; keep the remaining params and csrf()
as-is.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0587c53c-3ed0-4d9b-9d14-da22aced6a02

📥 Commits

Reviewing files that changed from the base of the PR and between 21ad19a and 11c702f.

📒 Files selected for processing (13)
  • src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java
  • src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java
  • src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java
  • src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java
  • src/main/resources/templates/fragments/cases.html
  • src/main/resources/templates/user/login.html
  • src/main/resources/templates/visa/edit-form.html
  • src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaViewControllerTest.java
✅ Files skipped from review due to trivial changes (1)
  • src/test/java/org/example/visacasemanagementsystem/user/controller/UserViewControllerTest.java
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/VisaLogService.java
  • src/main/java/org/example/visacasemanagementsystem/audit/service/UserLogService.java
  • src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java

…exception of role specific ones

Feature: 403 AccessDenied now properly redirects to the edit page
@Martin-E-Karlsson
Martin-E-Karlsson merged commit 8066468 into main Apr 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant