Skip to content

Commit 81ed483

Browse files
committed
Add content negotiation tests for CaseAssignedUserRolesController and enable support for application/json in GET /case-users endpoint.
1 parent 23e6ca1 commit 81ed483

2 files changed

Lines changed: 128 additions & 19 deletions

File tree

src/main/java/uk/gov/hmcts/ccd/v2/external/controller/CaseAssignedUserRolesController.java

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1515
import org.springframework.hateoas.MediaTypes;
1616
import org.springframework.http.HttpStatus;
17+
import org.springframework.http.MediaType;
1718
import org.springframework.http.ResponseEntity;
1819
import org.springframework.web.bind.annotation.DeleteMapping;
1920
import org.springframework.web.bind.annotation.GetMapping;
@@ -55,24 +56,7 @@
5556
import static uk.gov.hmcts.ccd.data.SecurityUtils.SERVICE_AUTHORIZATION;
5657

5758
@RestController
58-
/*
59-
NOTE: Explicitly set to application/hal+json to bypass a Spring Framework concurrency problem
60-
in AbstractJackson2HttpMessageConverter (Issue #36090).
61-
* Although the response bodies may suppress _links (via @JsonIgnore), we strictly enforce
62-
the HAL media type here for two critical reasons:
63-
* 1. CRASH PREVENTION: It forces Spring to select the specific HAL converter (fast path)
64-
instead of iterating over all converters to discover supported types. The iteration
65-
path triggers an ArrayIndexOutOfBoundsException on a corrupted LinkedHashMap
66-
during concurrent startup (Lazy Initialization race condition).
67-
* 2. COMPATIBILITY: It preserves the existing Content-Type header (application/hal+json)
68-
that clients expect, preventing contract breakage.
69-
* WARNING: DO NOT change this to MediaType.APPLICATION_JSON_VALUE or remove it
70-
without verifying that the upstream apps fix has been applied.
71-
*/
72-
@RequestMapping(
73-
path = "/",
74-
produces = MediaTypes.HAL_JSON_VALUE
75-
)
59+
@RequestMapping(path = "/")
7660
@ConditionalOnProperty(value = "ccd.conditional-apis.case-assigned-users-and-roles.enabled", havingValue = "true")
7761
public class CaseAssignedUserRolesController {
7862

@@ -214,8 +198,27 @@ public ResponseEntity<CaseAssignedUserRolesResponse> removeCaseUserRoles(
214198
* `414 URI Too Long` issues, see <a href="https://tools.hmcts.net/jira/browse/CCD-3588">CCD-3588</a>.
215199
*/
216200
@Deprecated(forRemoval = true)
201+
/*
202+
NOTE: Explicitly set to application/hal+json to bypass a Spring Framework concurrency problem
203+
in AbstractJackson2HttpMessageConverter (Issue #36090).
204+
* Although the response bodies may suppress _links (via @JsonIgnore), we strictly enforce
205+
the HAL media type here for two critical reasons:
206+
* 1. CRASH PREVENTION: It forces Spring to select the specific HAL converter (fast path)
207+
instead of iterating over all converters to discover supported types. The iteration
208+
path triggers an ArrayIndexOutOfBoundsException on a corrupted LinkedHashMap
209+
during concurrent startup (Lazy Initialization race condition).
210+
* 2. COMPATIBILITY: It preserves the existing Content-Type header (application/hal+json)
211+
that clients expect, preventing contract breakage.
212+
* NOTE: GET /case-users additionally produces application/json to maintain backwards
213+
compatibility with consuming services that send Accept: application/json. This is safe
214+
as the response body is identical in both cases — HAL-specific _links are suppressed
215+
via @JsonIgnore on CaseAssignedUserRolesResource.
216+
* WARNING: DO NOT change this to MediaType.APPLICATION_JSON_VALUE or remove it
217+
without verifying that the upstream apps fix has been applied.
218+
*/
217219
@GetMapping(
218-
path = "/case-users"
220+
path = "/case-users",
221+
produces = {MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE}
219222
)
220223
@Operation(
221224
summary = "Get Case-Assigned Users and Roles",
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package uk.gov.hmcts.ccd.v2.external.controller;
2+
3+
import com.google.common.collect.Lists;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Nested;
7+
import org.junit.jupiter.api.Test;
8+
import org.mockito.Mock;
9+
import org.mockito.MockitoAnnotations;
10+
import org.springframework.hateoas.MediaTypes;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.test.web.servlet.MockMvc;
13+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
14+
import uk.gov.hmcts.ccd.ApplicationParams;
15+
import uk.gov.hmcts.ccd.data.SecurityUtils;
16+
import uk.gov.hmcts.ccd.domain.model.std.CaseAssignedUserRole;
17+
import uk.gov.hmcts.ccd.domain.service.cauroles.CaseAssignedUserRolesOperation;
18+
import uk.gov.hmcts.ccd.domain.service.common.UIDService;
19+
20+
import java.util.List;
21+
22+
import static org.mockito.ArgumentMatchers.anyList;
23+
import static org.mockito.ArgumentMatchers.anyString;
24+
import static org.mockito.Mockito.when;
25+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
26+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
27+
28+
class CaseAssignedUserRolesControllerContentNegotiationTest {
29+
30+
private static final String CASE_ID_GOOD = "4444333322221111";
31+
32+
@Mock
33+
private ApplicationParams applicationParams;
34+
35+
@Mock
36+
private UIDService caseReferenceService;
37+
38+
@Mock
39+
private CaseAssignedUserRolesOperation caseAssignedUserRolesOperation;
40+
41+
@Mock
42+
private SecurityUtils securityUtils;
43+
44+
private MockMvc mockMvc;
45+
46+
@BeforeEach
47+
void setUp() {
48+
MockitoAnnotations.openMocks(this);
49+
50+
CaseAssignedUserRolesController controller = new CaseAssignedUserRolesController(
51+
applicationParams,
52+
caseReferenceService,
53+
caseAssignedUserRolesOperation,
54+
securityUtils
55+
);
56+
57+
mockMvc = MockMvcBuilders
58+
.standaloneSetup(controller)
59+
.build();
60+
61+
when(caseReferenceService.validateUID(anyString())).thenReturn(true);
62+
when(caseAssignedUserRolesOperation.findCaseUserRoles(anyList(), anyList()))
63+
.thenReturn(List.of(new CaseAssignedUserRole()));
64+
}
65+
66+
@Nested
67+
@DisplayName("GET /case-users content negotiation")
68+
class GetCaseUserRolesContentNegotiation {
69+
70+
@Test
71+
@DisplayName("should return 200 when Accept header is application/json")
72+
void getCaseUserRoles_shouldReturn200_whenAcceptHeaderIsApplicationJson() throws Exception {
73+
mockMvc.perform(get("/case-users")
74+
.param("case_ids", CASE_ID_GOOD)
75+
.accept(MediaType.APPLICATION_JSON))
76+
.andExpect(status().isOk());
77+
}
78+
79+
@Test
80+
@DisplayName("should return 200 when Accept header is application/hal+json")
81+
void getCaseUserRoles_shouldReturn200_whenAcceptHeaderIsHalJson() throws Exception {
82+
mockMvc.perform(get("/case-users")
83+
.param("case_ids", CASE_ID_GOOD)
84+
.accept(MediaTypes.HAL_JSON))
85+
.andExpect(status().isOk());
86+
}
87+
88+
@Test
89+
@DisplayName("should return 200 when Accept header is wildcard")
90+
void getCaseUserRoles_shouldReturn200_whenAcceptHeaderIsWildcard() throws Exception {
91+
mockMvc.perform(get("/case-users")
92+
.param("case_ids", CASE_ID_GOOD)
93+
.accept(MediaType.ALL))
94+
.andExpect(status().isOk());
95+
}
96+
97+
@Test
98+
@DisplayName("should return 406 when Accept header is unsupported media type")
99+
void getCaseUserRoles_shouldReturn406_whenAcceptHeaderIsUnsupported() throws Exception {
100+
mockMvc.perform(get("/case-users")
101+
.param("case_ids", CASE_ID_GOOD)
102+
.accept(MediaType.APPLICATION_XML))
103+
.andExpect(status().isNotAcceptable());
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)