Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.Map;
Expand All @@ -35,6 +36,12 @@ public class EtlController {
/** Response header that tells a keyed client whether a prior success was replayed. */
public static final String IDEMPOTENCY_REPLAYED_HEADER = "Idempotency-Replayed";

private static final MediaType UTF8_TEXT_PLAIN = new MediaType(
"text",
"plain",
StandardCharsets.UTF_8
);

private final EtlService etlService;
private final TargetConnectorDispatcher connectorDispatcher;

Expand Down Expand Up @@ -62,14 +69,14 @@ public EtlController(EtlService etlService, TargetConnectorDispatcher connectorD
*
* <p>The mapping does not constrain response negotiation to the successful representation.
* This allows callers that accept only {@code application/problem+json} to receive typed
* failure responses before a successful text body is selected. Typed request and data-access
* failures retain their dedicated handlers; only an unexpected runtime failure raised during
* the service invocation is wrapped as an ETL internal failure.</p>
* failure responses before a successful UTF-8 text body is selected. Typed request and
* data-access failures retain their dedicated handlers; only an unexpected runtime failure
* raised during the service invocation is wrapped as an ETL internal failure.</p>
*
* @param jsonInput UTF-8 JSON array request body
* @param idempotencyKey optional bounded client-generated retry key
* @param principal authenticated caller namespace for keyed requests
* @return existing newline-delimited plain-text success response, plus replay metadata when keyed
* @return newline-delimited UTF-8 plain-text success response, plus replay metadata when keyed
*/
@PostMapping("/process")
@Observed(name = "etl.process", contextualName = "etl-processing")
Expand Down Expand Up @@ -104,7 +111,7 @@ public ResponseEntity<String> processData(
}

ResponseEntity.BodyBuilder response = ResponseEntity.ok()
.contentType(MediaType.TEXT_PLAIN);
.contentType(UTF8_TEXT_PLAIN);
if (idempotencyKey != null) {
response.header(IDEMPOTENCY_REPLAYED_HEADER, Boolean.toString(replayed));
}
Expand All @@ -118,7 +125,7 @@ public ResponseEntity<String> processData(
* direct controller tests and embedded integrations while delegating to the mapped method.</p>
*
* @param jsonInput UTF-8 JSON array request body
* @return existing newline-delimited plain-text success response
* @return newline-delimited UTF-8 plain-text success response
*/
public ResponseEntity<String> processData(String jsonInput) {
return processData(jsonInput, null, null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.xtrmetl.etl.controller;

import com.xtrmetl.etl.connector.ConnectorProperties;
import com.xtrmetl.etl.connector.TargetConnectorDispatcher;
import com.xtrmetl.etl.connector.TargetConnectorRegistry;
import com.xtrmetl.etl.service.EtlService;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

class EtlControllerUtf8ResponseTest {

@Test
void successfulUnicodeResultDeclaresUtf8TextRepresentation() throws Exception {
EtlService etlService = mock(EtlService.class);
TargetConnectorDispatcher dispatcher = new TargetConnectorDispatcher(
new TargetConnectorRegistry(),
new ConnectorProperties()
);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
new EtlController(etlService, dispatcher)
)
.setControllerAdvice(new EtlApiProblemHandler())
.build();

String request = "[{\"id\":\"레코드_α\"}]";
String response = "Processed: 레코드_α";
when(etlService.processData(request)).thenReturn(response);

mockMvc.perform(post("/api/etl/process")
.contentType(MediaType.APPLICATION_JSON)
.content(request))
.andExpect(status().isOk())
.andExpect(header().string("Content-Type", "text/plain;charset=UTF-8"))
.andExpect(content().string(response));
}
}
Loading