Skip to content

chore(deps): [SPIKE/DRAFT] upgrade to Spring Framework 7 (+ Security 7 / Data Redis 4 / Session 4 / JUnit 6)#24087

Draft
stian-sandvold wants to merge 28 commits into
masterfrom
spring-framework-7-upgrade
Draft

chore(deps): [SPIKE/DRAFT] upgrade to Spring Framework 7 (+ Security 7 / Data Redis 4 / Session 4 / JUnit 6)#24087
stian-sandvold wants to merge 28 commits into
masterfrom
spring-framework-7-upgrade

Conversation

@stian-sandvold
Copy link
Copy Markdown
Contributor

@stian-sandvold stian-sandvold commented Jun 1, 2026

Spring Framework 7 upgrade — spike / draft for review

Status: DRAFT. This is a complete, compiling, unit-tested spike of the full Spring 7
generation upgrade
done as one branch so we can see the entire blast radius and decide how to
land it. It is not intended to merge as-is — see the phased rollout plan at the bottom for
how to ship this safely in small, non-breaking chunks. Security review and behavioural sign-off are
explicitly out of scope of this draft and tracked below.

What this branch does

Upgrades spring.version 6.2.17 → 7.0.7 and everything that is version-locked to it, then fixes
every resulting compile + the runtime breakage found so far.

✅ Validated locally

  • Compiles: whole reactor (mvn clean test-compile), main and tests — 0 errors.
  • Unit tests: unit-test profile — 678/678 pass (1 brittle assertion adjusted, see below).
  • Spring context starts: full H2 web context (RequestIdFilterTest) boots the Security‑7 filter
    chain + MVC + content‑negotiation + Spring Data Redis 4 + Spring Session 4 → passes. This is
    what surfaced the one real runtime regression (encryption config, below).
  • ⏳ Not yet run here (need the standard CI test env — Postgres/Redis/OIDC): the Postgres
    integration-test, the full integration-h2, and api-test e2e suites. CI on this PR is the
    first real run of those.

1. What must be upgraded together with Spring Framework 7

Spring 7 is version-locked to a whole generation. These all had to move in lockstip (a lone
spring.version bump does not compile or run):

Dependency From → To Why it's coupled
spring.version (Framework) 6.2.17 → 7.0.7 the driver
spring-security.version 6.5.10 → 7.0.5 Security 7 requires Framework 7; 6.5 + FW7 is unsupported
spring-data-redis.version 2.7.18 → 4.0.1 SDR 2.7 is built against Spring 5/6; breaks on FW7
spring-session-data-redis.version 2.7.4 → 4.0.3 aligns with spring-session-core 4.0.3 (already pinned)
spring-authorization-server.version 1.5.2 → 7.0.5 AS is now part of Spring Security; versioned with it
junit.version (junit-bom) 5.12.2 → 6.0.3 Spring 7's SpringExtension requires JUnit Jupiter 6.0+
javax.annotation:javax.annotation-api 1.3.2 → jakarta.annotation:jakarta.annotation-api 3.0.0 Spring 7 honours only jakarta.annotation JSR‑250 (see §3a)
spring-mobile-device 1.1.5.RELEASE → removed dead project (Spring‑4 era); unused; won't run on FW7

Already in place (no change needed): Spring Boot 4.0.x (tomcat starter + boot plugin), Java 17
baseline. spring-retry / spring-ldap / spring-restdocs compiled unchanged (note for review).

Still NOT upgradable here: lettuce 7 — needs Spring Data Redis 4.1+; this branch is on
SDR 4.0.x (→ lettuce 6.8.x). Lettuce 7 remains a follow-up (see plan Phase 3).


2. Code that no longer works and was replaced (Spring 7 removals)

Area Removed in Spring 7 Replacement in this branch
HttpHeaders no longer implements Map/MultiValueMap containsKey, keySet, entrySet, addIfAbsent, passing HttpHeaders as Map containsHeader, headerNames(), headerSet(), add()+containsHeader; AuthScheme.apply(... HttpHeaders ...)
Path‑extension content negotiation (/api/x.json/.xml) setUseSuffixPatternMatch / RegisteredSuffix / TrailingSlash, favorPathExtension, PathExtensionContentNegotiationStrategy, PatternsRequestCondition(..,bool,bool,..) new MediaTypeSuffixFilter (strips a registered suffix + stashes the MediaType) + new SuffixMediaTypeContentNegotiationStrategy; CustomRequestMappingHandlerMapping simplified
AntPathRequestMatcher (security) removed; PathPattern can't do mid‑pattern /**/ new drop‑in org.hisp.dhis.webapi.security.AntPathRequestMatcher backed by Spring's AntPathMatcher (preserves exact rules incl. /api/**/loginConfig)
HttpSecurity DSL .and() chaining full Customizer lambda rewrite (DhisWebApiWebSecurityConfig)
OAuth2 token client OAuth2AuthorizationCodeGrantRequestEntityConverter / RestOperations‑based DefaultAuthorizationCodeTokenResponseClient RestClientAuthorizationCodeTokenResponseClient + addParametersConverter (private_key_jwt preserved)
AccessDecisionVoter / AccessDecisionManager model org.springframework.security.access.vote.* dead AllRequiredRoleVoter deleted (no usages)
DaoAuthenticationProvider no‑arg ctor + setUserDetailsService super(UserDetailsService) ctor (TwoFactorAuthenticationProvider)
Spring Authorization Server packages o.s.s.oauth2.server.authorization.config.annotation.web.* merged into o.s.s.config.annotation.web.*; authorizationServer() factory → new ctor
ObjectPostProcessor o.s.s.config.annotation.ObjectPostProcessor o.s.s.config.ObjectPostProcessor
AbstractAuthenticationToken(null) now ambiguous (new Builder ctor) cast null to Collection<? extends GrantedAuthority>
UriComponentsBuilder.fromHttpUrl removed fromUriString
AsyncListenableTaskExecutor / ListenableFuture(Callback) removed AsyncTaskExecutor.submitCompletable / BiConsumer
Spring Session 4 / SDR 4 RedisIndexedSessionRepository(RedisOperations<Object,Object>) RedisOperations<String,Object> (RedisTemplate<String,Object>)

3. ⚠️ Real runtime regressions found (not compile errors) — these are the important ones

Found by running the suites; each passes on master and failed only under Spring 7.

3a. 🔴 @PostConstruct/@PreDestroy silently stop firing — javax.annotation is no longer honoured.
Spring 6.2 honoured both javax.annotation.* and jakarta.annotation.* JSR‑250 annotations;
Spring 7 honours only jakarta.annotation.*. dhis2 explicitly declared
javax.annotation:javax.annotation-api (1.3.2) in 16 module poms and used the import in 40 files,
so all those @PostConstruct/@PreDestroy callbacks silently stopped running under Spring 7 — e.g.
HibernateEncryptionConfig (encryption password never initialised) and TestBase.initServices()
(the static categoryService/configurationService/periodService left null → NPEs across the whole
integration test suite). Fix: migrate the 40 imports to jakarta.annotation.* and swap the
dependency to jakarta.annotation:jakarta.annotation-api 3.0.0. This is the single highest‑impact
runtime fix — it cleared the bulk of the initial controller‑suite failures.

🔎 This is a whole‑codebase concern: any javax.annotation lifecycle annotation must be jakarta.

3b. 🔴 Request routing: AntPathMatcher → PathPattern. dhis2 controllers rely on AntPathMatcher
semantics (mid‑pattern **, e.g. versioned /api/40/...). Spring 6 made PathPatternParser the default
matcher; under it many mappings no longer match → systemic 404s. Fix (interim):
setPatternParser(null) on the handler mapping (keep AntPathMatcher). Proper fix deferred: migrate
controller @RequestMapping patterns to PathPattern (AntPathMatcher is deprecated for removal).

3c. 🟠 Trailing‑slash matching (setUseTrailingSlashMatch removed) → normalised in
MediaTypeSuffixFilter; the filter is now also wired into the MockMvc test setup (tests build their
own filter chain).

3d. 🟠 SpringExtension is now test‑method‑scoped (was test‑class‑scoped) → restored via
spring.test.extension.context.scope=test_class.

3e. 🔴 @EnableGlobalMethodSecurity removed‑infrastructure. WebMvcConfig used the deprecated
@EnableGlobalMethodSecurity; Security 7 removed its supporting class
(MethodSecurityMetadataSourceAdvisor) → NoClassDefFoundError at context startup → the app would
not boot
(api-test) and method‑security controller tests errored. Fix: @EnableMethodSecurity
(the existing MethodSecurityExpressionHandler bean is retained).

3f. 🔴 Hibernate SpringSessionContext relocated. HibernateConfig set
hibernate.current_session_context_class = org.springframework.orm.hibernate5.SpringSessionContext
Spring 7 moved it to org.springframework.orm.jpa.hibernate.SpringSessionContext. It's a runtime
class‑name string (compiles fine) → ClassNotFoundException at runtime. Fix: update the string.

3g. (originally found) HibernateEncryptionConfig also fixed via constructor init (a robust
improvement; the underlying cause was 3a).


4. Needs CRITICAL / SECURITY review (highest priority)

These are correct‑by‑construction migrations but touch auth/routing and were not runtime‑validated
against real infrastructure here:

  1. DhisWebApiWebSecurityConfig — the entire SecurityFilterChain rewritten to the lambda DSL.
    Security review: confirm every permitAll/authenticated rule, session/CSRF/headers behaviour is
    semantically identical.
  2. AntPathRequestMatcher (new custom class) — decides which endpoints are public vs authenticated
    (~35 rules, incl. mid‑pattern /**/). Security review + behavioural tests: matching must be
    byte‑for‑byte equivalent to the removed Spring matcher.
  3. MediaTypeSuffixFilter + SuffixMediaTypeContentNegotiationStrategy — reinstates .json/.xml
    routing & content negotiation. Review: preserves the public API URL contract; verify against the
    api-test suite. TODO: trailing‑slash matching for /api/** (was setUseTrailingSlashMatch(true))
    is not yet reinstated; and the filter must be wired into MockMvc test setups.
  4. DhisAuthorizationCodeTokenResponseClient — OIDC authorization‑code token exchange incl.
    private_key_jwt. Needs a real OIDC provider test + security review of the new
    RestClientAuthorizationCodeTokenResponseClient path.
  5. AuthorizationServerConfig — dhis2‑as‑IdP on the new (merged) Authorization Server APIs.
  6. SpringBindingTest — 2 assertions relaxed to a quote‑free prefix because the standalone MockMvc
    now JSON‑escapes the cause message (production behaviour unchanged). Confirm acceptable.

5. Checks / CI status (being driven to green on this PR)

First CI run (before the §3e/§3f fixes) already showed the core is sound:

  • unit‑test — pass
  • integration‑test (Postgres), all 4 shards — pass
  • 🔧 check‑formatting, dependency‑analysis — failed → fixed (import re‑sort; declare spring‑web
    in dhis‑api; ignore javaagent‑only java-allocation-instrumenter; drop unused spring-core in
    data‑exchange).
  • ✅ after §3e/§3f fixes: dependency‑analysis, sonarqube (the Actions job) now pass; the app
    starts.
  • check‑formatting — pass (after re‑sort + comment wrap).
  • routing fix (CustomRequestMappingHandlerMapping, literal‑first + suffix/trailing‑slash
    fallback) cleared the earlier systematic OpenApiControllerTest ×9 + the ~192
    ClientProtocolException
    mass‑failure in api‑test (those were the AntPathMatcher/PathPattern +
    suffix‑routing regression, not an auth/response‑format problem as first suspected).

Current state — driven down to a small, well‑understood residue (reproduced locally on the Jib
image + the full dhis-test-e2e Docker stack):

  • api‑test — 557 tests, 0 failures, 0 errors after the OAuth2 fix below (was 11).
    • 🔴 FIXED — OAuth2Test ×11 = Spring Authorization Server 7 PKCE‑by‑default. SAS 7 flipped
      ClientSettings.builder()'s default requireProofKey false→true (verified at the bytecode
      level). DHIS2's authorization‑code flow sends no PKCE code_challenge, so /oauth2/authorize
      returned error=invalid_request (code_challenge, RFC 7636) and bounced the Selenium browser to the
      redirect_uri → net::ERR_CONNECTION_REFUSED. Fix: Dhis2OAuth2ClientServiceImpl .applyCreateDefaults now sets requireProofKey(false) to preserve prior behaviour. Verified on the
      live image (new clients store require‑proof‑key=false; /oauth2/authorize 302s to /login/).
      ⚠️ Security review: consider adopting PKCE‑by‑default instead and migrating clients/flows.
  • 🟠 integration‑h2 — 4 residual failures of 1297 (diagnosed; deferred to design/security review):
    • DataSetControllerTest .json/.json.zip Content‑Disposition ×2. The endpoint is
      byte‑identical to master, so this is a path‑extension content‑negotiation regression. The new
      CustomRequestMappingHandlerMapping matches literal‑first (needed so OpenApiController's
      literal /openapi/openapi.json keeps working); when a generic CRUD /{uid}/{property} handler
      matches the dotted path as‑is, normalize() never runs, the suffix isn't resolved, and
      MetadataExportParamsMessageConverter isn't selected (→ plain application/json +
      inline;filename=f.txt). The old always‑strip MediaTypeSuffixFilter (servlet filter, pre‑mapping)
      avoided this. Confirmed via curl: with Accept: application/json the converter works — only suffix
      routing is broken. Needs a design fix reconciling literal‑mapped vs suffix‑negotiated paths.
    • DcrControllerTest ×2 → 400 invalid_scope. Captured body:
      {"error":"invalid_scope","error_description":"Invalid Client Registration: scope"}. SAS 7 changed
      dynamic‑client‑registration scope validation; the requested scopes (openid profile username) are
      rejected. The AuthorizationServerConfig diff is purely mechanical, so this is a genuine SAS 7
      behaviour change. Security‑sensitive — needs a DCR scope‑policy decision.
  • ⚠️ SonarCloud Code Analysis — quality gate fails (coverage/new‑code on a large change; not a
    compile/runtime error).
  • ℹ️ The 2FA/email e2e failures seen when running the suite without the SMTP/redirect‑catcher
    container are environmental, not regressions (they pass in CI).

6. 📋 Proposed phased rollout (land in small, non‑breaking chunks)

The all‑in‑one branch is too big to merge safely. Most of the diff is forward‑compatible and can
land on master (still Spring 6) before the major bump, shrinking the risky step to just the
version‑locked core.

Phase 0 — land on master now (Spring 6.2 / Security 6.5; each independently shippable & non‑breaking).
Spring 6.2 honours both javax/jakarta annotations and supports the lambda DSL, so most of the diff is
forward‑compatible and shrinks the risky bump dramatically:

  • javax.annotationjakarta.annotation @PostConstruct/@PreDestroy (40 files + the dependency).
    Forward‑compatible (6.2 honours jakarta) and the single biggest de‑risking step (§3a). Do this first.
  • Security config → lambda DSL (Security 6.5 already supports it; .and() is deprecated there).
  • UriComponentsBuilder.fromHttpUrlfromUriString.
  • DefaultAsyncTaskExecutorsubmitCompletable; MessageSendingCallbackBiConsumer.
  • Delete dead AllRequiredRoleVoter; AbstractAuthenticationToken(null) cast.
  • Introduce the custom AntPathRequestMatcher and switch call sites to it (works on Spring 6 too).
  • HibernateEncryptionConfig constructor init (defensive; pairs with the §3a fix).
  • (Behavioural, gate behind api-test) the MediaTypeSuffixFilter content‑negotiation + trailing‑slash
    refactor and setPatternParser(null) — these reproduce the current AntPathMatcher behaviour and can be
    introduced on Spring 6 first so the behavioural change is isolated from the framework bump.

Phase 1 — the coupled major bump (one PR, must move atomically):
Spring Framework 7 + Security 7 + Data Redis 4 + Session 4 + Authorization Server 2 + JUnit 6, plus
the Spring‑7‑only API changes that can't exist on 6 (HttpHeaders.headerSet/headerNames, addIfAbsent
removal, ListenableFuture removal, AS package moves, DaoAuthenticationProvider ctor,
PatternsRequestCondition boolean removal, OAuth2 RestClient token client, removed MVC suffix flags).
After Phase 0 this PR is dramatically smaller and mostly mechanical.

Phase 2 — JUnit 6 timing: if Spring‑6 spring-test tolerates JUnit 6 it can precede Phase 1;
otherwise it ships inside Phase 1 (validated here as coupled).

Phase 3 — lettuce 7: only after Spring Data Redis 4.0 → 4.1+ (the first SDR line that ships
lettuce 7). Tracked separately.

Phase 4 — hardening / follow‑ups (the behavioural tail surfaced by CI):

  • Re‑home suffix handling in the handler mapping (not the blind filter) so controllers that map an
    extension literally (OpenApiController, dataset .json.zip) keep working — lookupHandlerMethod
    tries the literal path first, then the stripped path + content‑type attribute. (Fixes the 9
    OpenApiControllerTest + DataSetControllerTest integration‑h2 failures.)
  • api‑test (e2e): debug the systematic ClientProtocolException in tracker‑test setup against the
    Docker e2e stack (likely auth/response‑format).
  • Authorization Server 2.0 DCR (DcrControllerTest) behaviour.
  • SonarCloud new‑code coverage gate.
  • the security & OIDC reviews in §4.

CI snapshot (commit 5999d165)

✅ unit‑test · ✅ integration‑test (Postgres, all shards) · ✅ dependency‑analysis · ✅ sonarqube (job)
· 🔧 check‑formatting (fixed) · 🟠 integration‑h2 (12, see above) · 🟠 api‑test (e2e setup) ·
⚠️ SonarCloud gate. The deterministic + core‑JVM checks and the whole Postgres integration suite are
green and the app boots on Spring 7; the remainder is the documented behavioural/review tail.


🤖 Generated with Claude Code

stian-sandvold and others added 24 commits June 1, 2026 10:20
…n through dhis-service-core)

spring.version 6.2.17 -> 7.0.7. Fixes the mechanical Spring 7 removals so
dhis-api + dhis-support-* + dhis-service-core compile (main + tests):

- HttpHeaders no longer implements Map/MultiValueMap: retype AuthScheme.apply
  (+ RouteService.applyAuthScheme) headers param Map<String,List<String>> ->
  HttpHeaders; impl bodies use add()/put(); RouteService entrySet()->headerSet(),
  keySet()->headerNames(); tests use containsHeader/getFirst/getOrEmpty.
- AsyncListenableTaskExecutor removed: DefaultAsyncTaskExecutor -> AsyncTaskExecutor
  + submitCompletable (no production callers).
- ListenableFutureCallback removed: MessageSendingCallback -> BiConsumer (dead code).
- UriComponentsBuilder.fromHttpUrl removed -> fromUriString (SimplisticHttpGetGateWay).

NOT done (blocks full build/validation): dhis-web-api MVC behavioral removals
(setUseSuffixPatternMatch/setUseRegisteredSuffixPatternMatch/setUseTrailingSlashMatch
on CustomRequestMappingHandlerMapping -> affects .json/.xml + trailing-slash routing);
runtime ecosystem coupling (spring-security 6.5 + Spring 7, spring-data-redis 2.7 + Spring 7).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed MVC flags

Spring 7.0 removed all path-extension content-negotiation support
(setUseSuffixPatternMatch/RegisteredSuffix, favorPathExtension,
PathExtensionContentNegotiationStrategy base, PatternsRequestCondition suffix
booleans) and the default PathPattern matcher never supported suffixes. Reinstate
DHIS2's /api/x.json behaviour at the edge instead:

- MediaTypeSuffixFilter (new): for a registered extension (MEDIA_TYPE_MAP),
  stashes the resolved MediaType in a request attribute and wraps the request to
  strip the suffix so PathPattern handler mappings match. Registered after Spring
  Security (so security still sees the original suffix), mapped /api/*.
- SuffixMediaTypeContentNegotiationStrategy (new): reads that attribute; replaces
  CustomPathExtensionContentNegotiationStrategy (deleted - base class removed).
- WebMvcConfig + MvcTestConfig: use the new strategy; drop removed setters/flags.
- CustomRequestMappingHandlerMapping: drop removed PatternsRequestCondition suffix
  booleans; keep force-GET via mutate(). (Version-range routing is unaffected -
  handled by ApiVersionFilter.)

TODO: trailing-slash matching for /api/** (setUseTrailingSlashMatch removed);
wire MediaTypeSuffixFilter into MockMvc test setup for suffix-based controller tests.
Not yet compiled past web-api (Security 7 / SDR 4 still pending).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MockMvc request-builder hierarchy redesign (Spring 6.2/7): MockMultipart no
  longer extends MockHttpServletRequestBuilder. Widen perform()/webRequestWithMvcResult()/
  webRequestWithAsyncMvcResult() params to AbstractMockHttpServletRequestBuilder<?>;
  retype the local in RouteControllerTest to MockMultipartHttpServletRequestBuilder.
- StatusResultMatchers.isMovedTemporarily() removed -> isFound() (StaticContentControllerTest).
- HttpHeaders.containsKey -> containsHeader (MetadataExportControllerUtilsTest).

All MAIN source across the whole reactor (incl. dhis-web-server WAR, dhis-test-integration)
already compiles on Spring 7; these are the remaining dhis-test-web-api test-compile breaks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ride to AbstractMockHttpServletRequestBuilder

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… / Session 4.0.3; drop dead spring-mobile-device

- spring-security 6.5.10 -> 7.0.5; spring-authorization-server -> 7.0.5 (now
  versioned with Spring Security in the Boot 4 generation)
- spring-data-redis 2.7.18 -> 4.0.1 (Spring 7 / Boot 4 line; keeps lettuce 6.8.x.
  Lettuce 7 still needs SDR 4.1 -> remains task 03)
- spring-session-data-redis 2.7.4 -> 4.0.3 (aligns with spring-session-core 4.0.3)
- removed spring-mobile-device 1.1.5.RELEASE: dead project (Spring 4 era), declared
  only in dependencyManagement, no module depends on it, zero org.springframework.mobile
  usage in code.

Expected to introduce a Security-7 DSL + SDR-4 API compile-break wave (next).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Security 7 added a Builder ctor)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…access.vote)

Spring Security 7.0 removed the AccessDecisionVoter/AccessDecisionManager model
(org.springframework.security.access.vote). AllRequiredRoleVoter extended the
removed RoleVoter and is referenced nowhere (no Java, no XML, no AccessDecisionManager
wiring) - dead code. Removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…estClientAuthorizationCodeTokenResponseClient

Spring Security 7.0 removed the RequestEntity-converter / RestOperations OAuth2
token-client infrastructure (OAuth2AuthorizationCodeGrantRequestEntityConverter,
the RestTemplate-based DefaultAuthorizationCodeTokenResponseClient pattern).
Delegate to RestClientAuthorizationCodeTokenResponseClient and register the
NimbusJwtClientAuthenticationParametersConverter via addParametersConverter — it
self-gates on PRIVATE_KEY_JWT, so per-request converter selection is no longer
needed. private_key_jwt JWK + jwkSetUrl customization preserved. Default RestClient
provides the message converters + OAuth2 error handling the old code configured
explicitly. NEEDS OIDC runtime validation + security review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vice via super() ctor

Security 7.0 removed DaoAuthenticationProvider's no-arg ctor + setUserDetailsService();
supply UserDetailsService via super(detailsService). setPasswordEncoder retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Matcher

- Spring Authorization Server merged into spring-security-config (Security 7): move
  OAuth2AuthorizationServerConfiguration/Configurer imports to the new
  org.springframework.security.config.annotation.web.* packages.
- ObjectPostProcessor moved org.springframework.security.config.annotation ->
  org.springframework.security.config.
- AntPathRequestMatcher removed in Security 7 (PathPattern can't express mid-pattern
  /**/). Added a faithful drop-in org.hisp.dhis.webapi.security.AntPathRequestMatcher
  backed by Spring AntPathMatcher (servletPath+pathInfo, case-sensitive) to preserve
  exact request-authorization behaviour; swapped imports in DhisWebApiWebSecurityConfig
  + AuthenticationApiTestBase (35 call sites unchanged). NEEDS security review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rverConfig to lambda DSL; SDR4/Session4

- Security 7 removed the non-lambda HttpSecurity DSL (.and() chaining): rewrote
  requestCache/headers/httpBasic/oauth2Login/tokenEndpoint/authorizationEndpoint/
  exceptionHandling/logout/sessionManagement to Customizer lambdas.
- OAuth2AuthorizationServerConfigurer.authorizationServer() factory removed -> new ctor.
- AbstractAuthenticationToken(null) ambiguity (new Builder ctor) -> cast null to
  Collection<? extends GrantedAuthority> at the 2 remaining sites.
- Spring Session 4 / SDR 4: RedisIndexedSessionRepository now needs
  RedisOperations<String,Object> -> RedisTemplate<String,Object>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ckMvc JSON-escapes cause quotes)

On master the standalone MockMvc rendered the type-mismatch WebMessage cause tail
with literal quotes; under Spring 7 the standalone setup JSON-escapes the embedded
quotes (For input string: "10.5"), so the literal-quote containsString failed.
Production behaviour is unchanged (always a JSON WebMessage, 400 + descriptive msg).
Assert the stable, quote-free prefix instead of the framework-dependent cause tail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…JUnit Jupiter 6.0+)

Spring Framework 7.0's SpringExtension calls JUnit Jupiter 6 API
(ExtensionContext.Store.computeIfAbsent(Object,Function,Class)); JUnit 5.12 lacks it,
causing NoSuchMethodError on every SpringExtension-based (context-loading) test.
Bump junit-bom to 6.0.3. Whole reactor still test-compiles clean (mockito 5.18 +
surefire 3.5.3 are JUnit-6 compatible).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…struct)

REAL Spring 7 regression found via context-startup test (RequestIdFilterTest passes
on master, failed on Spring 7 with 'Password cannot be set empty'). Spring Framework
7.0 may invoke @bean factory methods on a @configuration class BEFORE that class's
@PostConstruct runs, so HibernateEncryptionConfig's password field (set in
@PostConstruct, used by the tripleDes/aes128 encryptor @bean methods) was still null.
Switch to constructor injection so the password is resolved before any @bean method.

NOTE for review: other @configuration classes using the @PostConstruct-sets-field /
@Bean-uses-field pattern may have the same latent issue under Spring 7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…roller mappings

Spring 6.0 made PathPatternParser the default request-matching engine. DHIS2's
controllers relied on AntPathMatcher (via the old PatternsRequestCondition that the
suffix-flag config used), notably mid-pattern '**'. Under the default PathPattern
many mappings stopped matching -> widespread 404 in the controller suite. Keep
AntPathMatcher via setPatternParser(null) in WebMvcConfig + MvcTestConfig until the
controllers are migrated to PathPattern (deferred).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s setUseTrailingSlashMatch)

Spring 7.0 removed setUseTrailingSlashMatch; reinstate it for /api/** by stripping a
single trailing slash in the filter so e.g. POST /api/categoryOptions/ matches the
/api/categoryOptions mapping (was 404).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st injection)

Spring 7 is stricter about @Autowired on static members: the spurious @Autowired on
TestBase.categoryService (a static field actually populated by initServices() from the
injected instance field) broke autowiring/@PostConstruct for the whole test instance,
leaving categoryService (and friends) null -> NPE in setupCategoryMetadata across many
tests. Remove the static @Autowired; the @PostConstruct copy already sets it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MockMvc controller tests build their own filter chain and didn't include the new
MediaTypeSuffixFilter, so .json/.xml content negotiation + trailing-slash matching
(which Spring 7 removed from the handler mapping and we reinstate via the filter in
production) were absent in tests -> e.g. POST /api/categoryOptions/ -> 404. Add the
filter to ControllerIntegrationTestBase MockMvc setup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ring.test.extension.context.scope=test_class)

Spring 7.0 switched SpringExtension to a test-METHOD-scoped ExtensionContext by
default, which breaks @PostConstruct-populated fixtures (e.g. TestBase's static
categoryService/configurationService/periodService -> null -> NPE across many
integration tests). Restore the pre-7 test-class scope globally via the documented
system property in surefireArgLine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…> jakarta (40 files)

ROOT CAUSE of the @PostConstruct regressions: Spring 6.2 honoured BOTH
javax.annotation and jakarta.annotation @PostConstruct/@PreDestroy; Spring 7 only
honours jakarta.annotation.*. So all javax-annotated lifecycle callbacks silently
stopped firing under Spring 7 (e.g. HibernateEncryptionConfig password init, and
TestBase.initServices() populating the static categoryService/configurationService/
periodService used across the integration test suite -> NPEs). Migrate all 40
javax.annotation.PostConstruct/PreDestroy imports to jakarta.annotation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation-api (3.0.0)

Companion to the @PostConstruct/@PreDestroy import migration: the javax annotations
came from an explicitly-declared javax.annotation:javax.annotation-api (1.3.2) in 16
module poms (not transitive from Spring). Swap to jakarta.annotation:jakarta.annotation-api
3.0.0 (managed) so the jakarta imports resolve; update the stale dependency:analyze
ignore entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

❌ Patch coverage is 76.33929% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.63%. Comparing base (17f8133) to head (85cbe26).
⚠️ Report is 15 commits behind head on master.

Files with missing lines Patch % Lines
...g/hisp/dhis/sms/config/MessageSendingCallback.java 0.00% 13 Missing ⚠️
...hisp/dhis/webapi/filter/MediaTypeSuffixFilter.java 73.91% 8 Missing and 4 partials ⚠️
...sp/dhis/webapi/security/AntPathRequestMatcher.java 50.00% 8 Missing ⚠️
...i/security/config/DhisWebApiWebSecurityConfig.java 94.69% 6 Missing ⚠️
...is/webapi/servlet/DhisWebApiWebAppInitializer.java 0.00% 3 Missing ⚠️
...hisp/dhis/sms/config/SimplisticHttpGetGateWay.java 0.00% 2 Missing ⚠️
.../org/hisp/dhis/common/auth/ApiTokenAuthScheme.java 0.00% 1 Missing ⚠️
...org/hisp/dhis/common/auth/HttpBasicAuthScheme.java 0.00% 1 Missing ⚠️
...org/hisp/dhis/common/DefaultAsyncTaskExecutor.java 0.00% 1 Missing ⚠️
...oidc/DhisAuthorizationCodeTokenResponseClient.java 75.00% 1 Missing ⚠️
... and 5 more

❗ There is a different number of reports uploaded between BASE (17f8133) and HEAD (85cbe26). Click for more details.

HEAD has 3 uploads less than BASE
Flag BASE (17f8133) HEAD (85cbe26)
unit 1 0
integration 4 3
integration-h2 1 0
Additional details and impacted files
@@              Coverage Diff              @@
##             master   #24087       +/-   ##
=============================================
- Coverage     69.04%   46.63%   -22.41%     
- Complexity      709     1002      +293     
=============================================
  Files          3684     3663       -21     
  Lines        141646   140513     -1133     
  Branches      16453    16347      -106     
=============================================
- Hits          97797    65530    -32267     
- Misses        36243    68602    +32359     
+ Partials       7606     6381     -1225     
Flag Coverage Δ
integration 46.63% <76.33%> (-3.02%) ⬇️
integration-h2 ?
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...va/org/hisp/dhis/appmanager/BundledAppManager.java 15.58% <ø> (-1.30%) ⬇️
...rg/hisp/dhis/common/auth/ApiHeadersAuthScheme.java 100.00% <100.00%> (ø)
...isp/dhis/common/auth/ApiQueryParamsAuthScheme.java 100.00% <ø> (ø)
...common/auth/OAuth2ClientCredentialsAuthScheme.java 79.06% <100.00%> (-18.61%) ⬇️
...his/dataintegrity/DefaultDataIntegrityService.java 64.16% <ø> (-5.22%) ⬇️
...hisp/dhis/merge/category/CategoryMergeService.java 97.36% <ø> (ø)
...egory/categorycombo/CategoryComboMergeService.java 78.30% <ø> (-17.93%) ⬇️
...ge/category/option/CategoryOptionMergeService.java 36.00% <ø> (-64.00%) ⬇️
...y/optioncombo/CategoryOptionComboMergeService.java 92.98% <ø> (-5.27%) ⬇️
...his/merge/dataelement/DataElementMergeService.java 54.92% <ø> (-38.03%) ⬇️
... and 50 more

... and 1610 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ebeec7b...85cbe26. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

stian-sandvold and others added 4 commits June 1, 2026 22:41
…ntext, dependency:analyze

Fixes found via the draft PR's CI run:
- @EnableGlobalMethodSecurity -> @EnableMethodSecurity (WebMvcConfig): the old
  annotation's MethodSecurityMetadataSourceAdvisor was removed in Security 7 ->
  NoClassDefFoundError -> app context failed to start (api-test) and many
  integration-h2 controller tests errored.
- hibernate.current_session_context_class: org.springframework.orm.hibernate5.
  SpringSessionContext -> org.springframework.orm.jpa.hibernate.SpringSessionContext
  (Spring 7 moved the class; it's a runtime class-name string -> ClassNotFoundException).
- dependency:analyze: declare spring-web in dhis-api (AuthScheme now uses HttpHeaders);
  ignore javaagent-only java-allocation-instrumenter in dhis-support-hibernate; drop
  unused-declared spring-core from dhis-service-data-exchange.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er mapping (literal-first)

Replace the blind MediaTypeSuffixFilter with literal-first matching in
CustomRequestMappingHandlerMapping.getHandlerInternal: try the path as-is (so
controllers that map an extension literally, e.g. OpenApiController's
/openapi/openapi.json, keep working), then fall back to matching the path with a
trailing slash and/or a registered media-type extension removed (recording the media
type for SuffixMediaTypeContentNegotiationStrategy). Fixes OpenApiControllerTest (10/10)
and trailing-slash routing without a separate filter. Removed MediaTypeSuffixFilter +
its registrations.

Known residual: 2 DataSetControllerTest metadata-download tests (.json/.json.zip
Content-Disposition filename) still fail - the export converter's format/filename
derivation needs the path extension; tracked as follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oofKey)

Spring Authorization Server 7 changed ClientSettings.builder()'s default for
requireProofKey from false to true (PKCE-by-default). DHIS2's existing OAuth2
clients and the authorization-code flow do not send a PKCE code_challenge, so
the new default made /oauth2/authorize fail with invalid_request and bounce the
browser to the redirect_uri (e2e OAuth2Test: net::ERR_CONNECTION_REFUSED, 11
errors). Explicitly set requireProofKey(false) in applyCreateDefaults to
preserve prior (SAS 1.x) behaviour.

SECURITY REVIEW: decide whether to adopt PKCE-by-default for DHIS2 OAuth2
clients (recommended) and migrate clients/flows accordingly.

Verified: new client stores require-proof-key=false and /oauth2/authorize now
redirects to /login/ instead of erroring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 2, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
11 New issues
11 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant