From caaa70115e662a136fc50184d8e9ee0fcbc49ec8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 20:15:38 +0900 Subject: [PATCH 1/2] test(api): require UTF-8 plain-text success response --- .../EtlControllerUtf8ResponseTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/controller/EtlControllerUtf8ResponseTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/controller/EtlControllerUtf8ResponseTest.java b/etl-service/src/test/java/com/xtrmetl/etl/controller/EtlControllerUtf8ResponseTest.java new file mode 100644 index 00000000..9ad7cb26 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/controller/EtlControllerUtf8ResponseTest.java @@ -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)); + } +} From 026d408427f2ead6c1735f2837cae2f73786b54f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 20:17:50 +0900 Subject: [PATCH 2/2] fix(api): declare UTF-8 text success responses --- .../xtrmetl/etl/controller/EtlController.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlController.java b/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlController.java index 1f932d46..e0bfbdea 100644 --- a/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlController.java +++ b/etl-service/src/main/java/com/xtrmetl/etl/controller/EtlController.java @@ -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; @@ -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; @@ -62,14 +69,14 @@ public EtlController(EtlService etlService, TargetConnectorDispatcher connectorD * *

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.

+ * 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.

* * @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") @@ -104,7 +111,7 @@ public ResponseEntity 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)); } @@ -118,7 +125,7 @@ public ResponseEntity processData( * direct controller tests and embedded integrations while delegating to the mapped method.

* * @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 processData(String jsonInput) { return processData(jsonInput, null, null);