Initial Python rewrite
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
|
||||
package com.example.mapper.conversation;
|
||||
|
||||
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.conversation.QueryInputDTO;
|
||||
import com.example.dto.dialogflow.conversation.QueryParamsDTO;
|
||||
import com.example.dto.dialogflow.conversation.TextInputDTO;
|
||||
import com.example.dto.dialogflow.notification.EventInputDTO;
|
||||
import com.google.cloud.dialogflow.cx.v3.DetectIntentRequest;
|
||||
import com.google.cloud.dialogflow.cx.v3.QueryInput;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DialogflowRequestMapperTest {
|
||||
|
||||
@InjectMocks
|
||||
private DialogflowRequestMapper dialogflowRequestMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(dialogflowRequestMapper, "defaultLanguageCode", "es");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapToDetectIntentRequestBuilder_withTextInput_shouldMapCorrectly() {
|
||||
// Given
|
||||
TextInputDTO textInputDTO = new TextInputDTO("Hola");
|
||||
QueryInputDTO queryInputDTO = new QueryInputDTO(textInputDTO, null, "es");
|
||||
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, null);
|
||||
|
||||
// When
|
||||
DetectIntentRequest.Builder builder = dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
|
||||
DetectIntentRequest request = builder.build();
|
||||
|
||||
// Then
|
||||
assertNotNull(request);
|
||||
assertTrue(request.hasQueryInput());
|
||||
QueryInput queryInput = request.getQueryInput();
|
||||
assertEquals("es", queryInput.getLanguageCode());
|
||||
assertTrue(queryInput.hasText());
|
||||
assertEquals("Hola", queryInput.getText().getText());
|
||||
assertFalse(queryInput.hasEvent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapToDetectIntentRequestBuilder_withEventInput_shouldMapCorrectly() {
|
||||
// Given
|
||||
EventInputDTO eventInputDTO = new EventInputDTO("welcome_event");
|
||||
QueryInputDTO queryInputDTO = new QueryInputDTO(null, eventInputDTO, "es");
|
||||
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, null);
|
||||
|
||||
// When
|
||||
DetectIntentRequest.Builder builder = dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
|
||||
DetectIntentRequest request = builder.build();
|
||||
|
||||
// Then
|
||||
assertNotNull(request);
|
||||
assertTrue(request.hasQueryInput());
|
||||
QueryInput queryInput = request.getQueryInput();
|
||||
assertEquals("es", queryInput.getLanguageCode());
|
||||
assertTrue(queryInput.hasEvent());
|
||||
assertEquals("welcome_event", queryInput.getEvent().getEvent());
|
||||
assertFalse(queryInput.hasText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapToDetectIntentRequestBuilder_withNoInput_shouldThrowException() {
|
||||
// Given
|
||||
QueryInputDTO queryInputDTO = new QueryInputDTO(null, null, "es");
|
||||
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, null);
|
||||
|
||||
// When & Then
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapToDetectIntentRequestBuilder_withParameters_shouldMapCorrectly() {
|
||||
// Given
|
||||
TextInputDTO textInputDTO = new TextInputDTO("Hola");
|
||||
QueryInputDTO queryInputDTO = new QueryInputDTO(textInputDTO, null, "es");
|
||||
Map<String, Object> parameters = Collections.singletonMap("param1", "value1");
|
||||
QueryParamsDTO queryParamsDTO = new QueryParamsDTO(parameters);
|
||||
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, queryParamsDTO);
|
||||
|
||||
// When
|
||||
DetectIntentRequest.Builder builder = dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
|
||||
DetectIntentRequest request = builder.build();
|
||||
|
||||
// Then
|
||||
assertNotNull(request);
|
||||
assertTrue(request.hasQueryParams());
|
||||
assertTrue(request.getQueryParams().hasParameters());
|
||||
assertEquals("value1", request.getQueryParams().getParameters().getFieldsMap().get("param1").getStringValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapToDetectIntentRequestBuilder_withNullRequestDTO_shouldThrowException() {
|
||||
// When & Then
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
dialogflowRequestMapper.mapToDetectIntentRequestBuilder(null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapToDetectIntentRequestBuilder_withDefaultLanguageCode_shouldMapCorrectly() {
|
||||
// Given
|
||||
TextInputDTO textInputDTO = new TextInputDTO("Hola");
|
||||
QueryInputDTO queryInputDTO = new QueryInputDTO(textInputDTO, null, null);
|
||||
DetectIntentRequestDTO requestDTO = new DetectIntentRequestDTO(queryInputDTO, null);
|
||||
|
||||
// When
|
||||
DetectIntentRequest.Builder builder = dialogflowRequestMapper.mapToDetectIntentRequestBuilder(requestDTO);
|
||||
DetectIntentRequest request = builder.build();
|
||||
|
||||
// Then
|
||||
assertNotNull(request);
|
||||
assertTrue(request.hasQueryInput());
|
||||
assertEquals("es", request.getQueryInput().getLanguageCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
|
||||
package com.example.mapper.conversation;
|
||||
|
||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||
import com.example.dto.dialogflow.conversation.QueryResultDTO;
|
||||
import com.google.cloud.dialogflow.cx.v3.DetectIntentResponse;
|
||||
import com.google.cloud.dialogflow.cx.v3.QueryResult;
|
||||
import com.google.cloud.dialogflow.cx.v3.ResponseMessage;
|
||||
import com.google.protobuf.Struct;
|
||||
import com.google.protobuf.Value;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DialogflowResponseMapperTest {
|
||||
|
||||
@InjectMocks
|
||||
private DialogflowResponseMapper dialogflowResponseMapper;
|
||||
|
||||
@Test
|
||||
void mapFromDialogflowResponse_shouldMapCorrectly() {
|
||||
// Given
|
||||
ResponseMessage.Text text1 = ResponseMessage.Text.newBuilder()
|
||||
.addAllText(Collections.singletonList("Hello")).build();
|
||||
ResponseMessage message1 = ResponseMessage.newBuilder().setText(text1).build();
|
||||
ResponseMessage.Text text2 = ResponseMessage.Text.newBuilder()
|
||||
.addAllText(Collections.singletonList("World")).build();
|
||||
ResponseMessage message2 = ResponseMessage.newBuilder().setText(text2).build();
|
||||
|
||||
Struct params = Struct.newBuilder()
|
||||
.putFields("param1", Value.newBuilder().setStringValue("value1").build())
|
||||
.putFields("param2", Value.newBuilder().setNumberValue(123).build())
|
||||
.build();
|
||||
|
||||
QueryResult queryResult = QueryResult.newBuilder()
|
||||
.addAllResponseMessages(Arrays.asList(message1, message2))
|
||||
.setParameters(params)
|
||||
.build();
|
||||
|
||||
DetectIntentResponse detectIntentResponse = DetectIntentResponse.newBuilder()
|
||||
.setResponseId("test-response-id")
|
||||
.setQueryResult(queryResult)
|
||||
.build();
|
||||
|
||||
// When
|
||||
DetectIntentResponseDTO responseDTO = dialogflowResponseMapper
|
||||
.mapFromDialogflowResponse(detectIntentResponse, "test-session-id");
|
||||
|
||||
// Then
|
||||
assertNotNull(responseDTO);
|
||||
assertEquals("test-response-id", responseDTO.responseId());
|
||||
|
||||
QueryResultDTO queryResultDTO = responseDTO.queryResult();
|
||||
assertNotNull(queryResultDTO);
|
||||
assertEquals("Hello World", queryResultDTO.responseText());
|
||||
|
||||
Map<String, Object> parameters = queryResultDTO.parameters();
|
||||
assertNotNull(parameters);
|
||||
assertEquals(2, parameters.size());
|
||||
assertEquals("value1", parameters.get("param1"));
|
||||
assertEquals(123.0, parameters.get("param2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapFromDialogflowResponse_withNoMessages_shouldReturnEmptyFulfillmentText() {
|
||||
// Given
|
||||
QueryResult queryResult = QueryResult.newBuilder()
|
||||
.build();
|
||||
|
||||
DetectIntentResponse detectIntentResponse = DetectIntentResponse.newBuilder()
|
||||
.setResponseId("test-response-id")
|
||||
.setQueryResult(queryResult)
|
||||
.build();
|
||||
|
||||
// When
|
||||
DetectIntentResponseDTO responseDTO = dialogflowResponseMapper
|
||||
.mapFromDialogflowResponse(detectIntentResponse, "test-session-id");
|
||||
|
||||
// Then
|
||||
assertNotNull(responseDTO);
|
||||
assertEquals("test-response-id", responseDTO.responseId());
|
||||
|
||||
QueryResultDTO queryResultDTO = responseDTO.queryResult();
|
||||
assertNotNull(queryResultDTO);
|
||||
assertEquals("", queryResultDTO.responseText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapFromDialogflowResponse_withNoParameters_shouldReturnEmptyMap() {
|
||||
// Given
|
||||
ResponseMessage.Text text = ResponseMessage.Text.newBuilder()
|
||||
.addAllText(Collections.singletonList("Hello")).build();
|
||||
ResponseMessage message = ResponseMessage.newBuilder().setText(text).build();
|
||||
|
||||
QueryResult queryResult = QueryResult.newBuilder()
|
||||
.addResponseMessages(message)
|
||||
.build();
|
||||
|
||||
DetectIntentResponse detectIntentResponse = DetectIntentResponse.newBuilder()
|
||||
.setResponseId("test-response-id")
|
||||
.setQueryResult(queryResult)
|
||||
.build();
|
||||
|
||||
// When
|
||||
DetectIntentResponseDTO responseDTO = dialogflowResponseMapper
|
||||
.mapFromDialogflowResponse(detectIntentResponse, "test-session-id");
|
||||
|
||||
// Then
|
||||
assertNotNull(responseDTO);
|
||||
assertEquals("test-response-id", responseDTO.responseId());
|
||||
|
||||
QueryResultDTO queryResultDTO = responseDTO.queryResult();
|
||||
assertNotNull(queryResultDTO);
|
||||
assertEquals("Hello", queryResultDTO.responseText());
|
||||
|
||||
Map<String, Object> parameters = queryResultDTO.parameters();
|
||||
assertNotNull(parameters);
|
||||
assertEquals(0, parameters.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.example.mapper.messagefilter;
|
||||
|
||||
import com.example.dto.dialogflow.conversation.ConversationMessageDTO;
|
||||
import com.example.dto.dialogflow.conversation.MessageType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class ConversationContextMapperTest {
|
||||
|
||||
@Test
|
||||
public void testCleanAgentMessage() throws Exception {
|
||||
ConversationContextMapper mapper = new ConversationContextMapper();
|
||||
Method method = ConversationContextMapper.class.getDeclaredMethod("cleanAgentMessage", String.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
String input = "Agent: ¡Seguro, déjame buscarlo para ti! 😉 El 'mejor' banco es " +
|
||||
"subjetivo y depende de sus necesidades financieras personales. Para determinar " +
|
||||
"cuál es el más adecuado para usted, considere los siguientes factores:\n" +
|
||||
"* **Comisiones y cargos**: Evalúe las tarifas por mantenimiento de cuenta, " +
|
||||
"transferencias, retiros en cajeros automáticos de otras redes, y otros servicios.\n" +
|
||||
"* **Tasas de interés**: Compare las tasas de interés ofrecidas en cuentas de " +
|
||||
"ahorro, depósitos a plazo fijo y préstamos.\n" +
|
||||
"* **Servicios y productos**: Verifique si el banco ofrece los productos que " +
|
||||
"necesita, como cuentas corrientes, cuentas de ahorro, tarjetas de crédito, " +
|
||||
"hipotecas, inversiones, etc.\n" +
|
||||
"* **Accesibilidad y conveniencia**: Considere la ubicación de sucursales y " +
|
||||
"cajeros automáticos, la calidad de la banca en línea y móvil, y el servicio al " +
|
||||
"cliente.\n" +
|
||||
"* **Tecnología**: Evalúe la facilidad de uso de sus plataformas digitales, la " +
|
||||
"seguridad y las herramientas de gestión financiera que ofrecen.\n" +
|
||||
"**Ejemplo**: Si usted realiza muchas transacciones en línea y rara vez visita una " +
|
||||
"sucursal, un banco con una excelente aplicación móvil y bajas comisiones por " +
|
||||
"transacciones digitales podría ser ideal. Si, por el contrario, prefiere la " +
|
||||
"atención personalizada, un banco con una red de sucursales amplia y un buen " +
|
||||
"servicio al cliente presencial sería más adecuado.\n" +
|
||||
"**Siguientes pasos**: Le recomendamos investigar y comparar al menos tres bancos " +
|
||||
"diferentes basándose en sus prioridades financieras. Revise sus sitios web, lea " +
|
||||
"las condiciones de sus productos y, si es posible, consulte opiniones de otros " +
|
||||
"usuarios para tomar una decisión informada. \n" +
|
||||
"{response=El 'mejor' banco es subjetivo y depende de sus necesidades financieras " +
|
||||
"personales. Para determinar cuál es el más adecuado para usted, considere los " +
|
||||
"siguientes factores:* **Comisiones y cargos**: Evalúe las tarifas por " +
|
||||
"mantenimiento de cuenta, transferencias, retiros en cajeros automáticos de " +
|
||||
"otras redes, y otros servicios.* **Tasas de interés**: Compare las tasas de " +
|
||||
"interés ofrecidas en cuentas de ahorro, depósitos a plazo fijo y préstamos." +
|
||||
"* **Servicios y productos**: Verifique si el banco ofrece los productos que " +
|
||||
"necesita, como cuentas corrientes, cuentas de ahorro, tarjetas de crédito, " +
|
||||
"hipotecas, inversiones, etc.* **Accesibilidad y conveniencia**: Considere la " +
|
||||
"ubicación de sucursales y cajeros automáticos, la calidad de la banca en línea y " +
|
||||
"móvil, y el servicio al cliente.* **Tecnología**: Evalúe la facilidad de uso " +
|
||||
"de sus plataformas digitales, la seguridad y las herramientas de gestión " +
|
||||
"financiera que ofrecen.**Ejemplo**: Si usted realiza muchas transacciones en línea " +
|
||||
"y rara vez visita una sucursal, un banco con una excelente aplicación móvil y " +
|
||||
"bajas comisiones por transacciones digitales podría ser ideal. Si, por el " +
|
||||
"contrario, prefiere la atención personalizada, un banco con una red de " +
|
||||
"sucursales amplia y un buen servicio al cliente presencial sería más adecuado." +
|
||||
"**Siguientes pasos**: Le recomendamos investigar y comparar al menos tres bancos " +
|
||||
"diferentes basándose en sus prioridades financieras. Revise sus sitios web, lea " +
|
||||
"las condiciones de sus productos y, si es posible, consulte opiniones de otros " +
|
||||
"Gente, tomen una decisión informada., telefono=123456789, pregunta_nueva=NO, " +
|
||||
"usuario_id=user_by_phone_123456789, historial=que son las capsulas?cual es la mejor " +
|
||||
"para mi?, query_inicial=Cual es el mejor banco?, canal=sigma, " +
|
||||
"$request.generative.confirmacion_ayuda=¡Seguro, déjame buscarlo para ti! 😉, " +
|
||||
"query=Cual es el mejor banco?, webhook_success=true, " +
|
||||
"$request.generative.respuesta_algo_mas=¿Te puedo echar la mano con otra cosa? ¡Tú dime! 😎, " +
|
||||
"conversacion_notificacion=false, nickname=John Doe, notificacion= }";
|
||||
|
||||
String expected = "Agent: ¡Seguro, déjame buscarlo para ti! 😉 El 'mejor' banco es " +
|
||||
"subjetivo y depende de sus necesidades financieras personales. Para determinar " +
|
||||
"cuál es el más adecuado para usted, considere los siguientes factores:\n" +
|
||||
"* **Comisiones y cargos**: Evalúe las tarifas por mantenimiento de cuenta, " +
|
||||
"transferencias, retiros en cajeros automáticos de otras redes, y otros servicios.\n" +
|
||||
"* **Tasas de interés**: Compare las tasas de interés ofrecidas en cuentas de " +
|
||||
"ahorro, depósitos a plazo fijo y préstamos.\n" +
|
||||
"* **Servicios y productos**: Verifique si el banco ofrece los productos que " +
|
||||
"necesita, como cuentas corrientes, cuentas de ahorro, tarjetas de crédito, " +
|
||||
"hipotecas, inversiones, etc.\n" +
|
||||
"* **Accesibilidad y conveniencia**: Considere la ubicación de sucursales y " +
|
||||
"cajeros automáticos, la calidad de la banca en línea y móvil, y el servicio al " +
|
||||
"cliente.\n" +
|
||||
"* **Tecnología**: Evalúe la facilidad de uso de sus plataformas digitales, la " +
|
||||
"seguridad y las herramientas de gestión financiera que ofrecen.\n" +
|
||||
"**Ejemplo**: Si usted realiza muchas transacciones en línea y rara vez visita una " +
|
||||
"sucursal, un banco con una excelente aplicación móvil y bajas comisiones por " +
|
||||
"transacciones digitales podría ser ideal. Si, por el contrario, prefiere la " +
|
||||
"atención personalizada, un banco con una red de sucursales amplia y un buen " +
|
||||
"servicio al cliente presencial sería más adecuado.\n" +
|
||||
"**Siguientes pasos**: Le recomendamos investigar y comparar al menos tres bancos " +
|
||||
"diferentes basándose en sus prioridades financieras. Revise sus sitios web, lea " +
|
||||
"las condiciones de sus productos y, si es posible, consulte opiniones de otros " +
|
||||
"usuarios para tomar una decisión informada.";
|
||||
String result = (String) method.invoke(mapper, input);
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToTextWithTruncation() {
|
||||
ConversationContextMapper mapper = new ConversationContextMapper();
|
||||
List<ConversationMessageDTO> messages = new ArrayList<>();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
messages.add(createMessage("This is message " + i, MessageType.USER));
|
||||
}
|
||||
for (int i = 1000; i < 2000; i++) {
|
||||
messages.add(createMessage("This is message " + i, MessageType.AGENT));
|
||||
}
|
||||
|
||||
String result = mapper.toTextWithTruncation(messages);
|
||||
assertTrue(result.length() > 0);
|
||||
assertTrue(result.getBytes(java.nio.charset.StandardCharsets.UTF_8).length <= 50 * 1024);
|
||||
}
|
||||
|
||||
private ConversationMessageDTO createMessage(String text, MessageType type) {
|
||||
return new ConversationMessageDTO(type, Instant.now(), text, null, null);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testToTextFromMessages_SystemNotification_ShouldUseParamText() {
|
||||
ConversationContextMapper mapper = new ConversationContextMapper();
|
||||
|
||||
Map<String, Object> params = new java.util.HashMap<>();
|
||||
params.put("notification_text", "Tu estado de cuenta está listo");
|
||||
|
||||
ConversationMessageDTO systemMessage = new ConversationMessageDTO(
|
||||
MessageType.SYSTEM,
|
||||
Instant.now(),
|
||||
"NOTIFICATION",
|
||||
params,
|
||||
"whatsapp"
|
||||
);
|
||||
|
||||
List<ConversationMessageDTO> messages = new java.util.ArrayList<>();
|
||||
messages.add(systemMessage);
|
||||
|
||||
// WHEN
|
||||
String result = mapper.toTextFromMessages(messages);
|
||||
System.out.println(result);
|
||||
// THEN
|
||||
assertEquals("System: Tu estado de cuenta está listo", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
|
||||
* Your use of it is subject to your agreement with Google.
|
||||
*/
|
||||
|
||||
package com.example.service.conversation;
|
||||
|
||||
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||
import com.example.dto.dialogflow.conversation.*;
|
||||
import com.example.dto.dialogflow.notification.NotificationDTO;
|
||||
import com.example.mapper.conversation.ConversationEntryMapper;
|
||||
import com.example.mapper.conversation.ExternalConvRequestMapper;
|
||||
import com.example.mapper.messagefilter.ConversationContextMapper;
|
||||
import com.example.mapper.messagefilter.NotificationContextMapper;
|
||||
import com.example.service.base.DialogflowClientService;
|
||||
import com.example.service.base.MessageEntryFilter;
|
||||
import com.example.service.base.NotificationContextResolver;
|
||||
import com.example.service.llm.LlmResponseTunerService;
|
||||
import com.example.service.notification.MemoryStoreNotificationService;
|
||||
import com.example.service.quickreplies.QuickRepliesManagerService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ConversationManagerServiceTest {
|
||||
|
||||
@Mock
|
||||
private ExternalConvRequestMapper externalRequestToDialogflowMapper;
|
||||
@Mock
|
||||
private DialogflowClientService dialogflowServiceClient;
|
||||
@Mock
|
||||
private FirestoreConversationService firestoreConversationService;
|
||||
@Mock
|
||||
private MemoryStoreConversationService memoryStoreConversationService;
|
||||
@Mock
|
||||
private QuickRepliesManagerService quickRepliesManagerService;
|
||||
@Mock
|
||||
private MessageEntryFilter messageEntryFilter;
|
||||
@Mock
|
||||
private MemoryStoreNotificationService memoryStoreNotificationService;
|
||||
@Mock
|
||||
private NotificationContextMapper notificationContextMapper;
|
||||
@Mock
|
||||
private ConversationContextMapper conversationContextMapper;
|
||||
@Mock
|
||||
private DataLossPrevention dataLossPrevention;
|
||||
@Mock
|
||||
private NotificationContextResolver notificationContextResolver;
|
||||
@Mock
|
||||
private LlmResponseTunerService llmResponseTunerService;
|
||||
@Mock
|
||||
private ConversationEntryMapper conversationEntryMapper;
|
||||
|
||||
@InjectMocks
|
||||
private ConversationManagerService conversationManagerService;
|
||||
|
||||
@Test
|
||||
void startNotificationConversation_shouldSaveResolvedContextAndReturnIt() {
|
||||
// Given
|
||||
String userId = "test-user";
|
||||
String userPhoneNumber = "1234567890";
|
||||
String userMessageText = "test message";
|
||||
String sessionId = "test-session";
|
||||
String resolvedContext = "resolved context";
|
||||
|
||||
ConversationContext context = new ConversationContext(userId, null, userMessageText, userPhoneNumber);
|
||||
DetectIntentRequestDTO request = new DetectIntentRequestDTO(null, null);
|
||||
NotificationDTO notification = new NotificationDTO("1", "1234567890", Instant.now(), "test text", "test_event", "es", Collections.emptyMap(), "active");
|
||||
ConversationSessionDTO session = ConversationSessionDTO.create(sessionId, userId, userPhoneNumber);
|
||||
|
||||
when(memoryStoreConversationService.getSessionByTelefono(userPhoneNumber)).thenReturn(Mono.just(session));
|
||||
when(memoryStoreConversationService.getMessages(anyString())).thenReturn(Flux.empty());
|
||||
when(conversationContextMapper.toTextFromMessages(any())).thenReturn("history");
|
||||
when(notificationContextMapper.toText(notification)).thenReturn("notification text");
|
||||
when(notificationContextResolver.resolveContext(anyString(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(resolvedContext);
|
||||
when(llmResponseTunerService.setValue(anyString(), anyString())).thenReturn(Mono.empty());
|
||||
when(memoryStoreConversationService.saveSession(any(ConversationSessionDTO.class))).thenReturn(Mono.empty());
|
||||
when(memoryStoreConversationService.saveMessage(anyString(), any(ConversationMessageDTO.class))).thenReturn(Mono.empty());
|
||||
when(firestoreConversationService.saveSession(any(ConversationSessionDTO.class))).thenReturn(Mono.empty());
|
||||
when(firestoreConversationService.saveMessage(anyString(), any(ConversationMessageDTO.class))).thenReturn(Mono.empty());
|
||||
when(conversationEntryMapper.toConversationMessageDTO(any(ConversationEntryDTO.class))).thenReturn(new ConversationMessageDTO(MessageType.USER, Instant.now(), "text", null, null));
|
||||
when(dialogflowServiceClient.detectIntent(anyString(), any(DetectIntentRequestDTO.class))).thenReturn(Mono.just(new DetectIntentResponseDTO(sessionId, new QueryResultDTO(resolvedContext, null), null)));
|
||||
|
||||
// When
|
||||
Mono<DetectIntentResponseDTO> result = conversationManagerService.startNotificationConversation(context, request, notification);
|
||||
|
||||
// Then
|
||||
StepVerifier.create(result)
|
||||
.expectNextMatches(response -> response.queryResult().responseText().equals(resolvedContext))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
|
||||
* Your use of it is subject to your agreement with Google.
|
||||
*/
|
||||
package com.example.service.integration_testing;
|
||||
|
||||
import com.example.service.base.MessageEntryFilter;
|
||||
import com.example.util.PerformanceTimer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("dev")
|
||||
@DisplayName("MessageEntryFilter Integration Tests")
|
||||
public class MessageEntryFilterIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MessageEntryFilter messageEntryFilter;
|
||||
|
||||
private static final String NOTIFICATION_JSON_EXAMPLE =
|
||||
"[{\"texto\": \"Tu estado de cuenta de Agosto esta listo\"}," +
|
||||
"{\"texto\": \"Tu pago ha sido procesado\"}]";
|
||||
|
||||
private static final String CONVERSATION_JSON_EXAMPLE =
|
||||
"{\"sessionId\":\"ec9f3731-59ac-4bd0-849e-f45fcc18436d\"," +
|
||||
"\"userId\":\"user_by_phone_0102030405060708\"," +
|
||||
"\"telefono\":\"0102030405060708\"," +
|
||||
"\"createdAt\":\"2025-08-06T20:35:05.123699404Z\"," +
|
||||
"\"lastModified\":\"2025-08-06T20:35:05.984574281Z\"," +
|
||||
"\"entries\":[{" +
|
||||
"\"type\":\"USUARIO\"," +
|
||||
"\"timestamp\":\"2025-08-06T20:35:05.123516916Z\"," +
|
||||
"\"text\":\"Hola que tal\"" +
|
||||
"},{" +
|
||||
"\"type\":\"SISTEMA\"," +
|
||||
"\"timestamp\":\"2025-08-06T20:35:05.967828173Z\"," +
|
||||
"\"text\":\"\\Hola! Bienvenido a Banorte, te saluda Beto. \\En que te puedo ayudar? \"," +
|
||||
"\"parameters\":{" +
|
||||
"\"canal\":\"banortec\"," +
|
||||
"\"telefono\":\"0102030405060708\"," +
|
||||
"\"pantalla_contexto\":\"transferencias\"," +
|
||||
"\"usuario_id\":\"user_by_phone_0102030405060708\"," +
|
||||
"\"nickname\":\"John Doe\"" +
|
||||
"}" +
|
||||
"}]" +
|
||||
"}";
|
||||
|
||||
private static final List<String> CONVERSATION_QUERIES = Arrays.asList(
|
||||
"Hola, ¿cómo estás?",
|
||||
"Qué tal, ¿qué hay de nuevo?",
|
||||
"¿Cuál es el pronóstico del tiempo para hoy?",
|
||||
"Me gustaría saber más sobre otro servicio",
|
||||
"Tengo una pregunta general"
|
||||
);
|
||||
|
||||
private static final List<String> NOTIFICATION_QUERIES = Arrays.asList(
|
||||
"¿Dónde puedo ver mi estado de cuenta?",
|
||||
//"Quiero saber mas",
|
||||
"Muéstrame mi estado de cuenta de este mes",
|
||||
"¿Qué dice la notificación del 1 de agosto?"
|
||||
);
|
||||
|
||||
@Test
|
||||
@DisplayName("Gemini should classify various conversational queries as CONVERSATION")
|
||||
void classifyMessage_integrationTest_shouldClassifyVariousQueriesAsConversation() {
|
||||
for (int i = 0; i < CONVERSATION_QUERIES.size(); i++) {
|
||||
String query = CONVERSATION_QUERIES.get(i);
|
||||
String testName = String.format("Gemini (CONVERSATION) - Query %d", i + 1);
|
||||
|
||||
String result = PerformanceTimer.timeExecution(
|
||||
testName,
|
||||
() -> messageEntryFilter.classifyMessage(query, null,null)
|
||||
);
|
||||
|
||||
assertEquals(MessageEntryFilter.CATEGORY_CONVERSATION, result,
|
||||
String.format("Assertion failed for query: '%s'", query));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Gemini should classify various notification queries as NOTIFICATION with context")
|
||||
void classifyMessage_integrationTest_shouldClassifyVariousQueriesAsNotificationWithContext() {
|
||||
for (int i = 0; i < NOTIFICATION_QUERIES.size(); i++) {
|
||||
String query = NOTIFICATION_QUERIES.get(i);
|
||||
String testName = String.format("Gemini (NOTIFICATION with context) - Query %d", i + 1);
|
||||
|
||||
String result = PerformanceTimer.timeExecution(
|
||||
testName,
|
||||
() -> messageEntryFilter.classifyMessage(query, NOTIFICATION_JSON_EXAMPLE,CONVERSATION_JSON_EXAMPLE)
|
||||
);
|
||||
|
||||
assertEquals(MessageEntryFilter.CATEGORY_NOTIFICATION, result,
|
||||
String.format("Assertion failed for query: '%s'", query));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Gemini should classify various conversational queries as CONVERSATION even with context")
|
||||
void classifyMessage_integrationTest_shouldClassifyVariousConversationalQueriesWithContext() {
|
||||
for (int i = 0; i < CONVERSATION_QUERIES.size(); i++) {
|
||||
String query = CONVERSATION_QUERIES.get(i);
|
||||
String testName = String.format("Gemini (CONVERSATION with context) - Query %d", i + 1);
|
||||
|
||||
String result = PerformanceTimer.timeExecution(
|
||||
testName,
|
||||
() -> messageEntryFilter.classifyMessage(query, NOTIFICATION_JSON_EXAMPLE,CONVERSATION_JSON_EXAMPLE)
|
||||
);
|
||||
|
||||
assertEquals(MessageEntryFilter.CATEGORY_CONVERSATION, result,
|
||||
String.format("Assertion failed for query: '%s'", query));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.example.service.integration_testing;
|
||||
|
||||
import com.example.service.base.NotificationContextResolver;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("dev")
|
||||
@DisplayName("NotificationContextResolver Live Tests")
|
||||
public class NotificationContextResolverLiveTest {
|
||||
|
||||
private String notificationsJson;
|
||||
private String conversationJson;
|
||||
private String queryInputText;
|
||||
private String metadataJson;
|
||||
|
||||
@Autowired
|
||||
private NotificationContextResolver notificationContextResolver;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
notificationsJson = "Hola :\n" +
|
||||
"Pasó algo con la captura de tu INE y no se completó tu *solicitud de tarjeta de crédito con folio *.\n"
|
||||
+
|
||||
"¡Reinténtalo cuando quieras! Solo toma en cuenta estos consejos:\n" +
|
||||
"🪪 Presenta tu INE original (no copias ni escaneos).\n" +
|
||||
"📅Revisa que esté vigente y sin tachaduras.\n" +
|
||||
"📷 Confirma que la fotografía sea clara.\n" +
|
||||
"🏠 Asegúrate de que la dirección sea legible.\n" +
|
||||
"Estamos listos para recibirte.\n";
|
||||
|
||||
conversationJson = "System: Hola :Pasó algo con la captura de tu INE y no se completó tu *solicitud de tarjeta de crédito con folio *.¡Reinténtalo cuando quieras! Solo toma en cuenta estos consejos:🪪 Presenta tu INE original (no copias ni escaneos).📅Revisa que esté vigente y sin tachaduras.📷 Confirma que la fotografía sea clara.🏠 Asegúrate de que la dirección sea legible.Estamos listos para recibirte.notification_po_contexto=campañaprueba, notification_po_id_campaña=campaña01, notification_po_id_aplicacion=TestSigma, notification_po_id_notificacion=Prueba2";
|
||||
queryInputText = "cual es el id de la notificaion?";
|
||||
metadataJson = "{\"contexto\":\"campañaprueba\",\"id_aplicacion\":\"TestSigma\",\"id_campaña\":\"campaña01\",\"id_notificacion\":\"Prueba2\",\"vigencia\":\"30/09/2025\"}";
|
||||
//metadataJson = "{}";
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should get live response from LLM and print it")
|
||||
public void shouldGetLiveResponseFromLlmAndPrintIt() {
|
||||
String result = notificationContextResolver.resolveContext(queryInputText, notificationsJson, conversationJson,
|
||||
metadataJson, "test_user", "test_session", "1234567890");
|
||||
System.out.println("Live LLM Response: " + result);
|
||||
assertNotNull(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.example.service.llm;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.data.redis.core.ReactiveValueOperations;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class LlmResponseTunerServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveRedisTemplate<String, String> reactiveStringRedisTemplate;
|
||||
|
||||
@Mock
|
||||
private ReactiveValueOperations<String, String> reactiveValueOperations;
|
||||
|
||||
@InjectMocks
|
||||
private LlmResponseTunerServiceImpl llmResponseTunerService;
|
||||
|
||||
private final String llmPreResponseCollectionName = "llm-pre-response:";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(reactiveStringRedisTemplate.opsForValue()).thenReturn(reactiveValueOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getValue_shouldReturnValueFromRedis() {
|
||||
String key = "test_key";
|
||||
String expectedValue = "test_value";
|
||||
|
||||
when(reactiveValueOperations.get(llmPreResponseCollectionName + key)).thenReturn(Mono.just(expectedValue));
|
||||
|
||||
StepVerifier.create(llmResponseTunerService.getValue(key))
|
||||
.expectNext(expectedValue)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void setValue_shouldSetValueInRedis() {
|
||||
String key = "test_key";
|
||||
String value = "test_value";
|
||||
|
||||
when(reactiveValueOperations.set(llmPreResponseCollectionName + key, value)).thenReturn(Mono.just(true));
|
||||
|
||||
StepVerifier.create(llmResponseTunerService.setValue(key, value))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.example.service.unit_testing;
|
||||
|
||||
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
|
||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||
import com.example.exception.DialogflowClientException;
|
||||
import com.example.mapper.conversation.DialogflowRequestMapper;
|
||||
import com.example.mapper.conversation.DialogflowResponseMapper;
|
||||
import com.example.service.base.DialogflowClientService;
|
||||
import com.google.api.gax.rpc.ApiException;
|
||||
import com.google.api.gax.rpc.StatusCode;
|
||||
import com.google.cloud.dialogflow.cx.v3.DetectIntentRequest;
|
||||
import com.google.cloud.dialogflow.cx.v3.DetectIntentResponse;
|
||||
import com.google.cloud.dialogflow.cx.v3.SessionsClient;
|
||||
import com.google.cloud.dialogflow.cx.v3.SessionsSettings;
|
||||
|
||||
import io.grpc.Status;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DialogflowClientServiceTest {
|
||||
|
||||
private static final String PROJECT_ID = "test-project";
|
||||
private static final String LOCATION = "us-central1";
|
||||
private static final String AGENT_ID = "test-agent";
|
||||
private static final String SESSION_ID = "test-session-123";
|
||||
|
||||
@Mock
|
||||
private DialogflowRequestMapper mockRequestMapper;
|
||||
@Mock
|
||||
private DialogflowResponseMapper mockResponseMapper;
|
||||
@Mock
|
||||
private SessionsClient mockSessionsClient;
|
||||
|
||||
private MockedStatic<SessionsClient> mockedStaticSessionsClient;
|
||||
|
||||
private DialogflowClientService dialogflowClientService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
mockedStaticSessionsClient = Mockito.mockStatic(SessionsClient.class);
|
||||
mockedStaticSessionsClient.when(() -> SessionsClient.create(any(SessionsSettings.class)))
|
||||
.thenReturn(mockSessionsClient);
|
||||
|
||||
dialogflowClientService = new DialogflowClientService(
|
||||
PROJECT_ID,
|
||||
LOCATION,
|
||||
AGENT_ID,
|
||||
mockRequestMapper,
|
||||
mockResponseMapper
|
||||
);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
mockedStaticSessionsClient.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_shouldInitializeClientSuccessfully() {
|
||||
assertNotNull(dialogflowClientService);
|
||||
mockedStaticSessionsClient.verify(() -> SessionsClient.create(any(SessionsSettings.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeSessionsClient_shouldCloseClient() {
|
||||
dialogflowClientService.closeSessionsClient();
|
||||
verify(mockSessionsClient, times(1)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectIntent_whenSuccess_shouldReturnMappedResponse() {
|
||||
// Arrange
|
||||
DetectIntentRequestDTO requestDTO = mock(DetectIntentRequestDTO.class);
|
||||
DetectIntentRequest.Builder requestBuilder = DetectIntentRequest.newBuilder();
|
||||
DetectIntentRequest finalRequest = DetectIntentRequest.newBuilder()
|
||||
.setSession(String.format("projects/%s/locations/%s/agents/%s/sessions/%s", PROJECT_ID, LOCATION, AGENT_ID, SESSION_ID))
|
||||
.build();
|
||||
DetectIntentResponse dfResponse = DetectIntentResponse.newBuilder().build();
|
||||
DetectIntentResponseDTO expectedResponseDTO = mock(DetectIntentResponseDTO.class);
|
||||
|
||||
when(mockRequestMapper.mapToDetectIntentRequestBuilder(requestDTO)).thenReturn(requestBuilder);
|
||||
when(mockSessionsClient.detectIntent(any(DetectIntentRequest.class))).thenReturn(dfResponse);
|
||||
when(mockResponseMapper.mapFromDialogflowResponse(dfResponse, SESSION_ID)).thenReturn(expectedResponseDTO);
|
||||
|
||||
// Act & Assert
|
||||
StepVerifier.create(dialogflowClientService.detectIntent(SESSION_ID, requestDTO))
|
||||
.expectNext(expectedResponseDTO)
|
||||
.verifyComplete();
|
||||
|
||||
verify(mockSessionsClient).detectIntent(finalRequest);
|
||||
verify(mockResponseMapper).mapFromDialogflowResponse(dfResponse, SESSION_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectIntent_whenRequestMapperFails_shouldReturnError() {
|
||||
DetectIntentRequestDTO requestDTO = mock(DetectIntentRequestDTO.class);
|
||||
when(mockRequestMapper.mapToDetectIntentRequestBuilder(requestDTO))
|
||||
.thenThrow(new IllegalArgumentException("Invalid mapping"));
|
||||
StepVerifier.create(dialogflowClientService.detectIntent(SESSION_ID, requestDTO))
|
||||
.expectError(IllegalArgumentException.class)
|
||||
.verify();
|
||||
|
||||
verify(mockSessionsClient, never()).detectIntent(any(DetectIntentRequest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectIntent_whenDialogflowApiThrowsApiException_shouldReturnDialogflowClientException() {
|
||||
DetectIntentRequestDTO requestDTO = mock(DetectIntentRequestDTO.class);
|
||||
DetectIntentRequest.Builder requestBuilder = DetectIntentRequest.newBuilder();
|
||||
|
||||
ApiException apiException = new ApiException(
|
||||
"API Error",
|
||||
null,
|
||||
new StatusCode() {
|
||||
@Override
|
||||
public Code getCode() {
|
||||
return Code.UNAVAILABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTransportCode() {
|
||||
return Status.Code.UNAVAILABLE;
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
when(mockRequestMapper.mapToDetectIntentRequestBuilder(requestDTO)).thenReturn(requestBuilder);
|
||||
when(mockSessionsClient.detectIntent(any(DetectIntentRequest.class))).thenThrow(apiException);
|
||||
|
||||
StepVerifier.create(dialogflowClientService.detectIntent(SESSION_ID, requestDTO))
|
||||
.expectError(DialogflowClientException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectIntent_withNullSessionId_shouldThrowNullPointerException() {
|
||||
DetectIntentRequestDTO requestDTO = mock(DetectIntentRequestDTO.class);
|
||||
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
dialogflowClientService.detectIntent(null, requestDTO);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectIntent_withNullRequest_shouldThrowNullPointerException() {
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
dialogflowClientService.detectIntent(SESSION_ID, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.example.exception.GeminiClientException;
|
||||
import com.example.service.base.GeminiClientService;
|
||||
import com.google.genai.Client;
|
||||
import com.google.genai.errors.GenAiIOException;
|
||||
import com.google.genai.types.Content;
|
||||
import com.google.genai.types.GenerateContentConfig;
|
||||
import com.google.genai.types.GenerateContentResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GeminiClientServiceTest {
|
||||
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Client geminiClient;
|
||||
|
||||
@InjectMocks
|
||||
private GeminiClientService geminiClientService;
|
||||
|
||||
private String prompt;
|
||||
private Float temperature;
|
||||
private Integer maxOutputTokens;
|
||||
private String modelName;
|
||||
private Float top_P;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
prompt = "Test prompt";
|
||||
temperature = 0.5f;
|
||||
maxOutputTokens = 100;
|
||||
modelName = "gemini-test-model";
|
||||
top_P=0.85f;
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateContent_whenApiSucceeds_returnsGeneratedText() throws GeminiClientException {
|
||||
// Arrange
|
||||
String expectedText = "This is the generated content.";
|
||||
GenerateContentResponse mockResponse = mock(GenerateContentResponse.class);
|
||||
when(mockResponse.text()).thenReturn(expectedText);
|
||||
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
|
||||
.thenReturn(mockResponse);
|
||||
|
||||
String actualText = geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P);
|
||||
assertEquals(expectedText, actualText);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateContent_whenApiResponseIsNull_throwsGeminiClientException() {
|
||||
// Arrange
|
||||
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
|
||||
.thenReturn(null);
|
||||
|
||||
GeminiClientException exception = assertThrows(GeminiClientException.class, () ->
|
||||
geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P)
|
||||
);
|
||||
|
||||
assertEquals("No content generated or unexpected response structure.", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateContent_whenResponseTextIsNull_throwsGeminiClientException() {
|
||||
GenerateContentResponse mockResponse = mock(GenerateContentResponse.class);
|
||||
when(mockResponse.text()).thenReturn(null);
|
||||
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
|
||||
.thenReturn(mockResponse);
|
||||
|
||||
GeminiClientException exception = assertThrows(GeminiClientException.class, () ->
|
||||
geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P)
|
||||
);
|
||||
|
||||
assertEquals("No content generated or unexpected response structure.", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateContent_whenGenAiIOExceptionOccurs_throwsGeminiClientException() {
|
||||
// Arrange
|
||||
String errorMessage = "Network issue";
|
||||
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
|
||||
.thenThrow(new GenAiIOException(errorMessage, new IOException()));
|
||||
|
||||
GeminiClientException exception = assertThrows(GeminiClientException.class, () ->
|
||||
geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().startsWith("An API communication issue occurred:"));
|
||||
assertTrue(exception.getMessage().contains(errorMessage));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateContent_whenUnexpectedExceptionOccurs_throwsGeminiClientException() {
|
||||
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
|
||||
.thenThrow(new RuntimeException("Something went wrong"));
|
||||
|
||||
GeminiClientException exception = assertThrows(GeminiClientException.class, () ->
|
||||
geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P)
|
||||
);
|
||||
|
||||
assertEquals("An unexpected issue occurred during content generation.", exception.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
|
||||
* Your use of it is subject to your agreement with Google.
|
||||
*/
|
||||
package com.example.service.unit_testing;
|
||||
|
||||
import com.example.service.base.GeminiClientService;
|
||||
import com.example.service.base.MessageEntryFilter;
|
||||
import com.example.util.PerformanceTimer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyFloat;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("MessageEntryFilter Unit Tests")
|
||||
public class MessageEntryFilterTest {
|
||||
|
||||
@Mock
|
||||
private GeminiClientService geminiService;
|
||||
|
||||
@InjectMocks
|
||||
private MessageEntryFilter messageEntryFilter;
|
||||
|
||||
private ListAppender<ILoggingEvent> listAppender;
|
||||
private static final String NOTIFICATION_JSON_EXAMPLE =
|
||||
"{\"idNotificacion\":\"4c2992d3-539d-4b28-8d52-cdea02cd1c75\"," +
|
||||
"\"timestampCreacion\":\"2025-08-01T16:14:02.301671204Z\"," +
|
||||
"\"texto\":\"Tu estado de cuenta de Agosto esta listo\"," +
|
||||
"\"nombreEventoDialogflow\":\"notificacion\"," +
|
||||
"\"codigoIdiomaDialogflow\":\"es\"," +
|
||||
"\"parametros\":{\"notificacion_texto\":\"Tu estado de cuenta de Agosto esta listo\",\"telefono\":\"555555555\"}}";
|
||||
|
||||
private static final String CONVERSATION_JSON_EXAMPLE =
|
||||
"{\"sessionId\":\"ec9f3731-59ac-4bd0-849e-f45fcc18436d\"," +
|
||||
"\"userId\":\"user_by_phone_0102030405060708\"," +
|
||||
"\"telefono\":\"0102030405060708\"," +
|
||||
"\"createdAt\":\"2025-08-06T20:35:05.123699404Z\"," +
|
||||
"\"lastModified\":\"2025-08-06T20:35:05.984574281Z\"," +
|
||||
"\"entries\":[{" +
|
||||
"\"type\":\"USUARIO\"," +
|
||||
"\"timestamp\":\"2025-08-06T20:35:05.123516916Z\"," +
|
||||
"\"text\":\"Hola que tal\"" +
|
||||
"},{" +
|
||||
"\"type\":\"SISTEMA\"," +
|
||||
"\"timestamp\":\"2025-08-06T20:35:05.967828173Z\"," +
|
||||
"\"text\":\"\\u00a1Hola! Bienvenido a Banorte, te saluda Beto. \\u00bfEn qu\\u00e9 te puedo ayudar? \\uD83D\\uDE0A\"," +
|
||||
"\"parameters\":{" +
|
||||
"\"canal\":\"banortec\"," +
|
||||
"\"telefono\":\"0102030405060708\"," +
|
||||
"\"pantalla_contexto\":\"transferencias\"," +
|
||||
"\"usuario_id\":\"user_by_phone_0102030405060708\"," +
|
||||
"\"nickname\":\"John Doe\"" +
|
||||
"}" +
|
||||
"}]" +
|
||||
"}";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Logger logger = (Logger) LoggerFactory.getLogger(MessageEntryFilter.class);
|
||||
listAppender = new ListAppender<>();
|
||||
listAppender.start();
|
||||
logger.addAppender(listAppender);
|
||||
}
|
||||
|
||||
private List<String> getLogMessages() {
|
||||
return listAppender.list.stream()
|
||||
.map(ILoggingEvent::getFormattedMessage)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should classify as CONVERSATION when Gemini responds with 'CONVERSATION'")
|
||||
void classifyMessage_shouldReturnConversation_whenGeminiRespondsConversation() throws Exception {
|
||||
String query = "Hola,como estas?";
|
||||
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
|
||||
.thenReturn("CONVERSATION");
|
||||
|
||||
String result = PerformanceTimer.timeExecution("ClassifyConversationTest",
|
||||
() -> messageEntryFilter.classifyMessage(query, null,null));
|
||||
|
||||
assertEquals(MessageEntryFilter.CATEGORY_CONVERSATION, result);
|
||||
|
||||
verify(geminiService, times(1)).generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat());
|
||||
|
||||
List<String> logMessages = getLogMessages();
|
||||
assertNotNull(logMessages.stream()
|
||||
.filter(m -> m.contains("Classified as CONVERSATION. Input: 'Hola,como estas?'"))
|
||||
.findFirst()
|
||||
.orElse(null), "Log message for successful classification not found.");
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@DisplayName("Should classify as NOTIFICATION when Gemini responds with 'NOTIFICATION' (with context)")
|
||||
void classifyMessage_shouldReturnNotification_whenGeminiRespondsNotificationWithContext() throws Exception {
|
||||
String query = "Donde puedo descargar mi estado de cuenta";
|
||||
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
|
||||
.thenReturn("NOTIFICATION");
|
||||
|
||||
String result = PerformanceTimer.timeExecution("ClassifyNotificationTest",
|
||||
() -> messageEntryFilter.classifyMessage(query, NOTIFICATION_JSON_EXAMPLE,CONVERSATION_JSON_EXAMPLE));
|
||||
|
||||
assertEquals(MessageEntryFilter.CATEGORY_NOTIFICATION, result);
|
||||
|
||||
verify(geminiService, times(1)).generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat());
|
||||
|
||||
List<String> logMessages = getLogMessages();
|
||||
assertNotNull(logMessages.stream()
|
||||
.filter(m -> m.contains("Classified as NOTIFICATION") && m.contains(query))
|
||||
.findFirst()
|
||||
.orElse(null), "Log message for successful classification not found.");
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return UNKNOWN if queryInputText is null")
|
||||
void classifyMessage_shouldReturnUnknown_whenQueryInputTextIsNull() throws Exception {
|
||||
String result = messageEntryFilter.classifyMessage(null, null,null);
|
||||
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
|
||||
verify(geminiService, times(0)).generateContent(any(), any(), any(), any(), any());
|
||||
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Query input text for classification is null or blank")).findFirst().orElse(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return UNKNOWN if queryInputText is blank")
|
||||
void classifyMessage_shouldReturnUnknown_whenQueryInputTextIsBlank() throws Exception {
|
||||
String result = messageEntryFilter.classifyMessage(" ", null,null);
|
||||
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
|
||||
verify(geminiService, times(0)).generateContent(any(), any(), any(), any(), any());
|
||||
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Query input text for classification is null or blank")).findFirst().orElse(null));
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return UNKNOWN if Gemini returns null")
|
||||
void classifyMessage_shouldReturnUnknown_whenGeminiReturnsNull() throws Exception {
|
||||
String query = "Any valid query";
|
||||
when(geminiService.generateContent(any(), any(), any(), any(), any())).thenReturn(null);
|
||||
|
||||
String result = messageEntryFilter.classifyMessage(query, null,null);
|
||||
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
|
||||
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Gemini returned an unrecognised classification or was null/blank")).findFirst().orElse(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return UNKNOWN if Gemini returns blank")
|
||||
void classifyMessage_shouldReturnUnknown_whenGeminiReturnsBlank() throws Exception {
|
||||
String query = "Any valid query";
|
||||
when(geminiService.generateContent(any(), any(), any(), any(), any())).thenReturn(" ");
|
||||
|
||||
String result = messageEntryFilter.classifyMessage(query, null, null);
|
||||
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
|
||||
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Gemini returned an unrecognised classification or was null/blank")).findFirst().orElse(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return UNKNOWN if Gemini returns an unexpected string")
|
||||
void classifyMessage_shouldReturnUnknown_whenGeminiReturnsUnexpectedString() throws Exception {
|
||||
String query = "Any valid query";
|
||||
when(geminiService.generateContent(any(), any(), any(), any(), any())).thenReturn("INVALID_RESPONSE");
|
||||
|
||||
String result = messageEntryFilter.classifyMessage(query, null,null);
|
||||
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
|
||||
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Gemini returned an unrecognised classification")).findFirst().orElse(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return ERROR if Gemini service throws an exception")
|
||||
void classifyMessage_shouldReturnError_whenGeminiServiceThrowsException() throws Exception {
|
||||
String query = "Query causing error";
|
||||
when(geminiService.generateContent(any(), any(), any(), any(), any()))
|
||||
.thenThrow(new RuntimeException("Gemini API error"));
|
||||
|
||||
String result = messageEntryFilter.classifyMessage(query, null,null);
|
||||
assertEquals(MessageEntryFilter.CATEGORY_ERROR, result);
|
||||
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Error during Gemini classification")).findFirst().orElse(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should include notification context in prompt when provided and not blank")
|
||||
void classifyMessage_shouldIncludeNotificationContextInPrompt() throws Exception {
|
||||
String query = "What's up?";
|
||||
|
||||
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
|
||||
.thenReturn("CONVERSATION");
|
||||
|
||||
messageEntryFilter.classifyMessage(query, NOTIFICATION_JSON_EXAMPLE,CONVERSATION_JSON_EXAMPLE);
|
||||
|
||||
verify(geminiService, times(1)).generateContent(
|
||||
org.mockito.ArgumentMatchers.argThat(prompt ->
|
||||
prompt.contains("Recent Notifications Context:") &&
|
||||
prompt.contains(NOTIFICATION_JSON_EXAMPLE) &&
|
||||
prompt.contains("User Input: 'What's up?'")
|
||||
),
|
||||
anyFloat(), anyInt(), any(String.class), anyFloat()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should NOT include notification context in prompt when provided but blank")
|
||||
void classifyMessage_shouldNotIncludeNotificationContextInPromptWhenBlank() throws Exception {
|
||||
String query = "What's up?";
|
||||
String notifications = " ";
|
||||
String conversations =" ";
|
||||
|
||||
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
|
||||
.thenReturn("CONVERSATION");
|
||||
|
||||
messageEntryFilter.classifyMessage(query, notifications,conversations);
|
||||
|
||||
verify(geminiService, times(1)).generateContent(
|
||||
org.mockito.ArgumentMatchers.argThat(prompt ->
|
||||
!prompt.contains("Recent Notifications Context:") &&
|
||||
prompt.contains("User Input: 'What's up?'")
|
||||
),
|
||||
anyFloat(), anyInt(), any(String.class), anyFloat()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should NOT include notification context in prompt when null")
|
||||
void classifyMessage_shouldNotIncludeNotificationContextInPromptWhenNull() throws Exception {
|
||||
String query = "What's up?";
|
||||
String notifications = null;
|
||||
String conversations = null;
|
||||
|
||||
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
|
||||
.thenReturn("CONVERSATION");
|
||||
|
||||
messageEntryFilter.classifyMessage(query, notifications, conversations);
|
||||
|
||||
verify(geminiService, times(1)).generateContent(
|
||||
org.mockito.ArgumentMatchers.argThat(prompt ->
|
||||
!prompt.contains("Recent Notifications Context:") &&
|
||||
prompt.contains("User Input: 'What's up?'")
|
||||
),
|
||||
anyFloat(), anyInt(), any(String.class), anyFloat()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.example.service.unit_testing;
|
||||
|
||||
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
|
||||
import com.example.dto.dialogflow.conversation.*;
|
||||
import com.example.dto.quickreplies.QuestionDTO;
|
||||
import com.example.dto.quickreplies.QuickReplyDTO;
|
||||
import com.example.mapper.conversation.ConversationEntryMapper;
|
||||
import com.example.service.conversation.ConversationManagerService;
|
||||
import com.example.service.conversation.FirestoreConversationService;
|
||||
import com.example.service.conversation.MemoryStoreConversationService;
|
||||
import com.example.service.quickreplies.QuickRepliesManagerService;
|
||||
import com.example.service.quickreplies.QuickReplyContentService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class QuickRepliesManagerServiceTest {
|
||||
|
||||
@Mock private MemoryStoreConversationService memoryStoreConversationService;
|
||||
@Mock private FirestoreConversationService firestoreConversationService;
|
||||
@Mock private QuickReplyContentService quickReplyContentService;
|
||||
@Mock private ConversationManagerService conversationManagerService;
|
||||
@Mock private ConversationEntryMapper conversationEntryMapper;
|
||||
|
||||
private QuickRepliesManagerService quickRepliesManagerService;
|
||||
|
||||
// Test Data
|
||||
private final String PHONE = "5555555555";
|
||||
private final String SESSION_ID = "session-123";
|
||||
private final String USER_ID = "user_by_phone_5555555555";
|
||||
private final String CONTEXTO = "pagos";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
quickRepliesManagerService = new QuickRepliesManagerService(
|
||||
conversationManagerService,
|
||||
memoryStoreConversationService,
|
||||
firestoreConversationService,
|
||||
quickReplyContentService,
|
||||
conversationEntryMapper
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("manageConversation - Count 0 - NO MATCH: Should clear context and delegate to Dialogflow")
|
||||
void manageConversation_Count0_NoMatch_ShouldDelegate() {
|
||||
// 1. SETUP: User typed "Hola", but context "pagos" is active.
|
||||
ExternalConvRequestDTO request = new ExternalConvRequestDTO("Hola", new UsuarioDTO(PHONE, "Nick"), "whatsapp", ConversationEntryType.CONVERSACION, null);
|
||||
ConversationSessionDTO session = new ConversationSessionDTO(SESSION_ID, USER_ID, PHONE, Instant.now(), Instant.now(), "last", CONTEXTO);
|
||||
|
||||
// Mock Session Retrieval
|
||||
when(memoryStoreConversationService.getSessionByTelefono(PHONE)).thenReturn(Mono.just(session));
|
||||
|
||||
// Mock History: Only the SYSTEM message (The Menu) exists. Count = 0.
|
||||
ConversationMessageDTO sysMsg = new ConversationMessageDTO(MessageType.SYSTEM, Instant.now(), "Menu...", null, "whatsapp");
|
||||
when(memoryStoreConversationService.getMessages(SESSION_ID)).thenReturn(Flux.just(sysMsg));
|
||||
|
||||
// Mock QR Content: The menu has options, but "Hola" is NOT one of them.
|
||||
QuestionDTO q1 = new QuestionDTO("Ver Saldo", "desc", "Tu saldo is 10");
|
||||
QuickReplyDTO qrDto = new QuickReplyDTO("Header", "Body", "Btn", "Section", List.of(q1));
|
||||
when(quickReplyContentService.getQuickReplies(CONTEXTO)).thenReturn(Mono.just(qrDto));
|
||||
|
||||
// Mock Orchestrator Delegation (The expected outcome)
|
||||
DetectIntentResponseDTO delegatedResponse = new DetectIntentResponseDTO("df-response", new QueryResultDTO("Hola soy Beto", null));
|
||||
when(memoryStoreConversationService.updateSession(any())).thenReturn(Mono.empty()); // Clearing context
|
||||
when(conversationManagerService.manageConversation(request)).thenReturn(Mono.just(delegatedResponse));
|
||||
|
||||
// 2. EXECUTE
|
||||
StepVerifier.create(quickRepliesManagerService.manageConversation(request))
|
||||
.expectNext(delegatedResponse)
|
||||
.verifyComplete();
|
||||
|
||||
// 3. VERIFY
|
||||
// Ensure we cleared the context
|
||||
verify(memoryStoreConversationService).updateSession(argThat(s -> s.pantallaContexto() == null));
|
||||
// Ensure we called the normal conversation manager
|
||||
verify(conversationManagerService).manageConversation(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("manageConversation - Count 0 - MATCH: Should return QR Answer")
|
||||
void manageConversation_Count0_Match_ShouldAnswer() {
|
||||
// 1. SETUP: User typed "Ver Saldo" (Valid Option)
|
||||
ExternalConvRequestDTO request = new ExternalConvRequestDTO("Ver Saldo", new UsuarioDTO(PHONE, "Nick"), "whatsapp", ConversationEntryType.CONVERSACION, null);
|
||||
ConversationSessionDTO session = new ConversationSessionDTO(SESSION_ID, USER_ID, PHONE, Instant.now(), Instant.now(), "last", CONTEXTO);
|
||||
|
||||
when(memoryStoreConversationService.getSessionByTelefono(PHONE)).thenReturn(Mono.just(session));
|
||||
|
||||
// Count 0 (Last message was System)
|
||||
ConversationMessageDTO sysMsg = new ConversationMessageDTO(MessageType.SYSTEM, Instant.now(), "Menu...", null, "whatsapp");
|
||||
when(memoryStoreConversationService.getMessages(SESSION_ID)).thenReturn(Flux.just(sysMsg));
|
||||
|
||||
// Valid Option exists
|
||||
QuestionDTO q1 = new QuestionDTO("Ver Saldo", "desc", "Tu saldo es 100 pesos");
|
||||
QuickReplyDTO qrDto = new QuickReplyDTO("Header", "Body", "Btn", "Section", List.of(q1));
|
||||
when(quickReplyContentService.getQuickReplies(CONTEXTO)).thenReturn(Mono.just(qrDto));
|
||||
|
||||
// Mocks for saving the conversation turn
|
||||
mockPersistence();
|
||||
when(memoryStoreConversationService.updateSession(any())).thenReturn(Mono.empty());
|
||||
|
||||
// 2. EXECUTE
|
||||
StepVerifier.create(quickRepliesManagerService.manageConversation(request))
|
||||
.assertNext(response -> {
|
||||
// Expect the pre-defined answer from the JSON
|
||||
assert response.queryResult().responseText().equals("Tu saldo es 100 pesos");
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
// Verify we did NOT delegate
|
||||
verify(conversationManagerService, never()).manageConversation(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("manageConversation - Count 1 - NO MATCH: Should save User Msg, Clear, and Delegate")
|
||||
void manageConversation_Count1_NoMatch_ShouldDelegate() {
|
||||
// 1. SETUP: User typed "Gracias" (Not an option)
|
||||
ExternalConvRequestDTO request = new ExternalConvRequestDTO("Gracias", new UsuarioDTO(PHONE, "Nick"), "whatsapp", ConversationEntryType.CONVERSACION, null);
|
||||
ConversationSessionDTO session = new ConversationSessionDTO(SESSION_ID, USER_ID, PHONE, Instant.now(), Instant.now(), "last", CONTEXTO);
|
||||
|
||||
when(memoryStoreConversationService.getSessionByTelefono(PHONE)).thenReturn(Mono.just(session));
|
||||
|
||||
// Mock History: System msg -> User msg (Invalid) -> Now this is the 2nd user msg (Count 1 logic)
|
||||
// Wait, logic says count is messages AFTER last init.
|
||||
// Sys (Init) -> User("Hola" - ignored previously) -> Current Request("Gracias")
|
||||
ConversationMessageDTO sysMsg = new ConversationMessageDTO(MessageType.SYSTEM, Instant.now(), "Menu...", null, "whatsapp");
|
||||
ConversationMessageDTO userMsg1 = new ConversationMessageDTO(MessageType.USER, Instant.now(), "Hola", null, "whatsapp");
|
||||
when(memoryStoreConversationService.getMessages(SESSION_ID)).thenReturn(Flux.just(sysMsg, userMsg1));
|
||||
|
||||
QuestionDTO q1 = new QuestionDTO("Ver Saldo", "desc", "Tu saldo es 10");
|
||||
QuickReplyDTO qrDto = new QuickReplyDTO("Header", "Body", "Btn", "Section", List.of(q1));
|
||||
when(quickReplyContentService.getQuickReplies(CONTEXTO)).thenReturn(Mono.just(qrDto));
|
||||
|
||||
// Mock persistence for the user message (Manual save in the else block)
|
||||
mockPersistence();
|
||||
|
||||
// Mock Delegation
|
||||
DetectIntentResponseDTO delegatedResponse = new DetectIntentResponseDTO("df-response", new QueryResultDTO("De nada", null));
|
||||
when(memoryStoreConversationService.updateSession(any())).thenReturn(Mono.empty());
|
||||
when(conversationManagerService.manageConversation(request)).thenReturn(Mono.just(delegatedResponse));
|
||||
|
||||
// 2. EXECUTE
|
||||
StepVerifier.create(quickRepliesManagerService.manageConversation(request))
|
||||
.expectNext(delegatedResponse)
|
||||
.verifyComplete();
|
||||
|
||||
// 3. VERIFY
|
||||
// Ensure manual save was called (verifying flow of `persistConversationTurn`)
|
||||
verify(memoryStoreConversationService, times(1)).saveMessage(eq(SESSION_ID), any());
|
||||
verify(memoryStoreConversationService).updateSession(argThat(s -> s.pantallaContexto() == null));
|
||||
verify(conversationManagerService).manageConversation(request);
|
||||
}
|
||||
|
||||
// Helper to mock the complex save chain
|
||||
private void mockPersistence() {
|
||||
when(conversationEntryMapper.toConversationMessageDTO(any())).thenReturn(new ConversationMessageDTO(MessageType.USER, Instant.now(), "text", null, "wa"));
|
||||
when(memoryStoreConversationService.saveSession(any())).thenReturn(Mono.empty());
|
||||
when(memoryStoreConversationService.saveMessage(anyString(), any())).thenReturn(Mono.empty());
|
||||
when(firestoreConversationService.saveSession(any())).thenReturn(Mono.empty());
|
||||
when(firestoreConversationService.saveMessage(anyString(), any())).thenReturn(Mono.empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
|
||||
package com.example.service.unit_testing;
|
||||
|
||||
import com.example.dto.quickreplies.QuestionDTO;
|
||||
import com.example.dto.quickreplies.QuickReplyDTO;
|
||||
import com.example.service.quickreplies.QuickReplyContentService;
|
||||
import com.google.api.core.ApiFuture;
|
||||
import com.google.cloud.firestore.CollectionReference;
|
||||
import com.google.cloud.firestore.DocumentReference;
|
||||
import com.google.cloud.firestore.DocumentSnapshot;
|
||||
import com.google.cloud.firestore.Firestore;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class QuickReplyContentServiceTest {
|
||||
|
||||
@Mock
|
||||
private Firestore firestore;
|
||||
|
||||
@Mock
|
||||
private CollectionReference collectionReference;
|
||||
|
||||
@Mock
|
||||
private DocumentReference documentReference;
|
||||
|
||||
@Mock
|
||||
private ApiFuture<DocumentSnapshot> apiFuture;
|
||||
|
||||
@Mock
|
||||
private DocumentSnapshot documentSnapshot;
|
||||
|
||||
@InjectMocks
|
||||
private QuickReplyContentService quickReplyContentService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getQuickReplies_success() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
String collectionId = "home";
|
||||
String header = "home_header";
|
||||
String body = "home_body";
|
||||
String button = "home_button";
|
||||
String headerSection = "home_header_section";
|
||||
List<Map<String, Object>> preguntas = Collections.singletonList(
|
||||
Map.of("titulo", "title", "descripcion", "description", "respuesta", "response")
|
||||
);
|
||||
List<QuestionDTO> questionDTOs = Collections.singletonList(
|
||||
new QuestionDTO("title", "description", "response")
|
||||
);
|
||||
QuickReplyDTO expected = new QuickReplyDTO(header, body, button, headerSection, questionDTOs);
|
||||
|
||||
when(firestore.collection("artifacts")).thenReturn(collectionReference);
|
||||
when(collectionReference.document("default-app-id")).thenReturn(documentReference);
|
||||
when(documentReference.collection("quick-replies")).thenReturn(collectionReference);
|
||||
when(collectionReference.document(collectionId)).thenReturn(documentReference);
|
||||
when(documentReference.get()).thenReturn(apiFuture);
|
||||
when(apiFuture.get()).thenReturn(documentSnapshot);
|
||||
when(documentSnapshot.exists()).thenReturn(true);
|
||||
when(documentSnapshot.getString("header")).thenReturn(header);
|
||||
when(documentSnapshot.getString("body")).thenReturn(body);
|
||||
when(documentSnapshot.getString("button")).thenReturn(button);
|
||||
when(documentSnapshot.getString("header_section")).thenReturn(headerSection);
|
||||
when(documentSnapshot.get("preguntas")).thenReturn(preguntas);
|
||||
|
||||
// When
|
||||
Mono<QuickReplyDTO> result = quickReplyContentService.getQuickReplies(collectionId);
|
||||
|
||||
// Then
|
||||
StepVerifier.create(result)
|
||||
.expectNext(expected)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getQuickReplies_emptyWhenNotFound() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
String collectionId = "non-existent-collection";
|
||||
|
||||
when(firestore.collection("artifacts")).thenReturn(collectionReference);
|
||||
when(collectionReference.document("default-app-id")).thenReturn(documentReference);
|
||||
when(documentReference.collection("quick-replies")).thenReturn(collectionReference);
|
||||
when(collectionReference.document(collectionId)).thenReturn(documentReference);
|
||||
when(documentReference.get()).thenReturn(apiFuture);
|
||||
when(apiFuture.get()).thenReturn(documentSnapshot);
|
||||
when(documentSnapshot.exists()).thenReturn(false);
|
||||
|
||||
// When
|
||||
Mono<QuickReplyDTO> result = quickReplyContentService.getQuickReplies(collectionId);
|
||||
|
||||
// Then
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getQuickReplies_emptyWhenCollectionIdIsBlank() {
|
||||
// Given
|
||||
String collectionId = "";
|
||||
|
||||
// When
|
||||
Mono<QuickReplyDTO> result = quickReplyContentService.getQuickReplies(collectionId);
|
||||
|
||||
// Then
|
||||
StepVerifier.create(result)
|
||||
.expectNext(new QuickReplyDTO("empty", null, null, null, Collections.emptyList()))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
1
src.bak/test/resources/application-test.properties
Normal file
1
src.bak/test/resources/application-test.properties
Normal file
@@ -0,0 +1 @@
|
||||
spring.cloud.gcp.firestore.database-id=firestoredb
|
||||
14
src.bak/test/resources/logback-test.xml
Normal file
14
src.bak/test/resources/logback-test.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user