diff --git a/docker-compose/dependent-docker-compose.yml b/docker-compose/dependent-docker-compose.yml index c1a0213e..810467bf 100644 --- a/docker-compose/dependent-docker-compose.yml +++ b/docker-compose/dependent-docker-compose.yml @@ -70,4 +70,18 @@ services: depends_on: - database - redis - - mock-identity-system \ No newline at end of file + - mock-identity-system + + esignet-ui: + image: 'mosipdev/oidc-ui:release-1.5.x' + user: root + ports: + - 3000:3000 + environment: + - container_user=mosip + - DEFAULT_WELLKNOWN=%5B%7B%22name%22%3A%22OpenID%20Configuration%22%2C%22value%22%3A%22%2F.well-known%2Fopenid-configuration%22%7D%2C%7B%22name%22%3A%22Jwks%20Json%22%2C%22value%22%3A%22%2F.well-known%2Fjwks.json%22%7D%2C%7B%22name%22%3A%22Authorization%20Server%22%2C%22value%22%3A%22%2F.well-known%2Foauth-authorization-server%22%7D%5D + - SIGN_IN_WITH_ESIGNET_PLUGIN_URL=https://raw.githubusercontent.com/mosip/artifactory-ref-impl/master/artifacts/src/mosip-plugins/sign-in-with-esignet/sign-in-with-esignet.zip + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + depends_on: + - esignet \ No newline at end of file diff --git a/postman-collection/README.md b/postman-collection/README.md index 8eaab9d4..a8c8110e 100644 --- a/postman-collection/README.md +++ b/postman-collection/README.md @@ -1,4 +1,70 @@ -## Usage of [stomp_websocket.py](stomp_websocket.py) +## Usage of [ws_client.py](ws_client.py) +eKYC verification process is carried out through WebSocket connection and as postman currently does not support export of WS +collections, we have created [ws_client.py](ws_client.py) script. -TODO \ No newline at end of file +This script is a simple Python WebSocket client that connects to a specified WebSocket server, subscribes to a topic using +the STOMP protocol, and allows the user to send messages to that topic. + +## Overview of the Script +User Input: + +The script starts by asking the user for the WebSocket server URL, slot ID, and cookie value. + +If you are running eSignet signup service in local, then the url will be "ws://localhost:8089/v1/signup/ws" +Slot ID and IDV_SLOT_ALOTTED cookie value should be taken from 'http://localhost:8088/v1/signup/identity-verification/slot' endpoint response. + +## WebSocket Callbacks: + +Several callback functions handle different events during the WebSocket connection lifecycle: +on_message: Called when a message is received from the server. +on_error: Called when an error occurs during the WebSocket operation. +on_close: Called when the WebSocket connection is closed. +on_open: Called when the WebSocket connection is successfully established. + +## STOMP Protocol Frames: + +The script uses the STOMP protocol to communicate with the WebSocket server, sending: +A CONNECT frame to initiate the STOMP connection. +A SUBSCRIBE frame to listen for messages on a specified topic (based on the user-provided slot_id). +A SEND frame to send messages from the user input to the specified destination. + +## Threading for User Input: + +The send_user_input function runs in a separate thread, allowing the main thread to continue processing incoming messages while waiting for user input. +The user can enter messages to send to the server or type "exit" to close the connection. + +## WebSocket Connection Management: + +The start_ws_client function sets up the WebSocket connection using the websocket-client library, specifying the URI and headers (including cookies). +The connection is established with ws.run_forever(), which keeps the connection alive and processes incoming messages. + + +## How to use the script? +1. Install Required Library: Ensure you have the websocket-client library installed. You can install it using: + +`pip install websocket-client` + +2. Run the Script: Execute the script in your terminal or command prompt: + +`python ws_client.py` + +3. Provide Input: When prompted, enter the base URL (WebSocket server address), slot ID, and cookie value. + +4. Sending Messages: When prompted, to enter message to send, type the message as below, there are 3 different messages + +START step message -> `{"slotId":"slotId","stepCode":"START","frames":[{"frame":"","order":"0"}]}` + +Other step messages -> `{"slotId":"slotId","stepCode":"","frames":[{"frame":"","order":"0"}]}` + +END step message -> `{"slotId":"slotId","stepCode":"END","frames":[{"frame":"","order":"0"}]}` + +5. Receiving Messages: Any messages sent from the server to the subscribed topic will be printed to the console as they are received. + + +## Example interaction + + +![img.png](interaction_1.png) + +![img_1.png](interaction_2.png) \ No newline at end of file diff --git a/postman-collection/interaction_1.png b/postman-collection/interaction_1.png new file mode 100644 index 00000000..f241b94b Binary files /dev/null and b/postman-collection/interaction_1.png differ diff --git a/postman-collection/interaction_2.png b/postman-collection/interaction_2.png new file mode 100644 index 00000000..7799369b Binary files /dev/null and b/postman-collection/interaction_2.png differ diff --git a/postman-collection/stomp_websocket.py b/postman-collection/stomp_websocket.py deleted file mode 100644 index 67fddb5e..00000000 --- a/postman-collection/stomp_websocket.py +++ /dev/null @@ -1,84 +0,0 @@ -import asyncio -import websockets -import sys -import uuid - -# Function to construct the WebSocket connection URL with slotId as a query parameter -def construct_ws_url(base_url, slot_id): - return f"{base_url}?slotId={slot_id}" - -# Function to send messages or disconnect based on user input -async def handle_user_input(websocket): - while True: - choice = input("Do you want to send a message or disconnect? (Type 'send' to send, 'disconnect' to disconnect): ").strip().lower() - - if choice == 'send': - message = input("Enter the message to send: ") - if message: - # Construct the SEND frame to the specific destination - send_frame = f"SEND\ndestination:/v1/signup/ws/process-frame\ncontent-type:application/json\n\n{message}\x00" - await websocket.send(send_frame) - print(f"Message sent: {send_frame}") - else: - print("No message entered. Skipping send.") - - elif choice == 'disconnect': - print("Disconnecting from the server...") - await websocket.close() - break - - else: - print("Invalid option. Please type 'send' or 'disconnect'.") - -# Function to receive messages from the server -async def receive_message(websocket): - while True: - try: - response = await websocket.recv() - print(f"Message received from server: {response}") - except websockets.exceptions.ConnectionClosed: - print("Connection closed by the server.") - break - except Exception as e: - print(f"Error receiving message: {e}") - break - -async def connect_to_websocket(base_url, slot_id, cookie): - uri = construct_ws_url(base_url, slot_id) - - # Define headers with the cookie - headers = { - 'Cookie': f'{cookie}' - } - - # Connect to WebSocket using the provided URI and headers - async with websockets.connect(uri, extra_headers=headers) as websocket: - print(f"Connected to WebSocket at {uri}") - - # Generate a unique subscription ID - subscription_id = str(uuid.uuid4()) - - # Subscribe to the /topic/slotId destination - subscribe_frame = f"SUBSCRIBE\nid:{subscription_id}\ndestination:/topic/{slot_id}\n\n\x00" - await websocket.send(subscribe_frame) - print(f"{subscribe_frame}") - - # Create two tasks: one for sending messages or disconnecting, and another for receiving messages - receive_task = asyncio.create_task(receive_message(websocket)) - send_task = asyncio.create_task(handle_user_input(websocket)) - - # Run both tasks concurrently - await asyncio.gather(receive_task, send_task) - -# Entry point: Get baseUrl, slotId, and cookie from user input -if __name__ == "__main__": - if len(sys.argv) != 4: - print("Usage: python stomp_websocket_send_or_disconnect.py ") - sys.exit(1) - - base_url = sys.argv[1] - slot_id = sys.argv[2] - cookie = sys.argv[3] - - # Start the WebSocket connection - asyncio.get_event_loop().run_until_complete(connect_to_websocket(base_url, slot_id, cookie)) diff --git a/postman-collection/ws_client.py b/postman-collection/ws_client.py new file mode 100644 index 00000000..19fe4d28 --- /dev/null +++ b/postman-collection/ws_client.py @@ -0,0 +1,70 @@ +import websocket +import threading +import sys + +base_url=input("Enter the base URL (ws://localhost:8089/v1/signup/ws): ") +slot_id=input("Enter the slotId: ") +cookie=input("Enter the cookie value: ") + +def on_message(ws, message): + print("===================") + print(f"Received {message}") + print("===================") + +def on_error(ws, error): + print("Error:", error) + +def on_close(ws, close_status_code, close_msg): + print("Connection closed:", close_status_code, close_msg) + +def on_open(ws): + # Send STOMP CONNECT frame + connect_frame = "CONNECT\naccept-version:1.2\n\n\x00" + ws.send(connect_frame) + print(f"{connect_frame}") + + # Subscribe to the /topic/slotId destination + subscribe_frame = f"SUBSCRIBE\nid:sub-0\ndestination:/topic/{slot_id}\n\n\x00" + ws.send(subscribe_frame) + print(f"{subscribe_frame}") + + # Start a new thread to take user input and send messages to the WebSocket + threading.Thread(target=send_user_input, args=(ws,)).start() + +def send_user_input(ws): + try: + while True: + user_input = input("Enter a message to send: ") + if user_input.lower() == "exit": + print("Closing connection...") + ws.close() + break + + # Send user input as a message to the WebSocket + send_frame = f"SEND\ndestination:/v1/signup/ws/process-frame\ncontent-type:application/json\n\n{user_input}\x00" + ws.send(send_frame) + print(f"{send_frame}") + + except Exception as e: + print("Error sending message:", e) + +# WebSocket connection +def start_ws_client(): + uri = f"{base_url}?slotId={slot_id}" # Replace with your WebSocket server's URI + headers = {"Cookie": f"IDV_SLOT_ALLOTTED={cookie}"} # Replace with any necessary headers + + ws = websocket.WebSocketApp( + uri, + header=headers, + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close + ) + + # Run the WebSocket with a blocking loop + ws.run_forever() + +# Run the subscribe function +if __name__ == "__main__": + start_ws_client() diff --git a/signup-service/src/main/java/io/mosip/signup/config/WebSocketConfig.java b/signup-service/src/main/java/io/mosip/signup/config/WebSocketConfig.java index 4f35ed33..fed2c88f 100644 --- a/signup-service/src/main/java/io/mosip/signup/config/WebSocketConfig.java +++ b/signup-service/src/main/java/io/mosip/signup/config/WebSocketConfig.java @@ -22,13 +22,14 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/topic"); registry.setApplicationDestinationPrefixes("/v1/signup/ws"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //By default, only same origin requests are allowed, should take the origin from properties - registry.addEndpoint("/ws").setAllowedOrigins("*").setHandshakeHandler(webSocketHandshakeHandler); + registry.addEndpoint("/ws") + .setAllowedOrigins("*") + .setHandshakeHandler(webSocketHandshakeHandler); } } diff --git a/signup-service/src/main/java/io/mosip/signup/services/IdentityVerifierFactory.java b/signup-service/src/main/java/io/mosip/signup/services/IdentityVerifierFactory.java index b8cc48ee..7c678a41 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/IdentityVerifierFactory.java +++ b/signup-service/src/main/java/io/mosip/signup/services/IdentityVerifierFactory.java @@ -26,14 +26,11 @@ public class IdentityVerifierFactory { public IdentityVerifierPlugin getIdentityVerifier(String id) { - log.info("Request to fetch identity verifier with id : {}", id); - log.info("List of identity verifiers found : {}", identityVerifiers); + log.debug("Request to fetch identity verifier with id : {} in the available list of verifiers: {}", id, identityVerifiers); Optional result = identityVerifiers.stream() .filter(idv -> idv.getVerifierId().equals(id) ) .findFirst(); - log.info("Identity verifiers result : {}", result); - if(result.isEmpty()) throw new IdentityVerifierException(PLUGIN_NOT_FOUND); diff --git a/signup-service/src/main/java/io/mosip/signup/services/WebSocketHandler.java b/signup-service/src/main/java/io/mosip/signup/services/WebSocketHandler.java index 52624900..24787c72 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/WebSocketHandler.java +++ b/signup-service/src/main/java/io/mosip/signup/services/WebSocketHandler.java @@ -100,6 +100,7 @@ public void processVerificationResult(IdentityVerificationResult identityVerific return; } + log.debug("Analysis result published to /topic/{}", identityVerificationResult.getId()); simpMessagingTemplate.convertAndSend("/topic/"+identityVerificationResult.getId(), identityVerificationResult); //END step marks verification process completion diff --git a/signup-service/src/test/java/io/mosip/signup/helper/CryptoHelperTest.java b/signup-service/src/test/java/io/mosip/signup/helper/CryptoHelperTest.java new file mode 100644 index 00000000..2404ed6e --- /dev/null +++ b/signup-service/src/test/java/io/mosip/signup/helper/CryptoHelperTest.java @@ -0,0 +1,77 @@ +package io.mosip.signup.helper; + +import io.mosip.esignet.core.util.IdentityProviderUtil; +import io.mosip.signup.services.CacheUtilService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class CryptoHelperTest { + + @InjectMocks + private CryptoHelper cryptoHelper; + + @Mock + private CacheUtilService cacheUtilService; + + private static String symmetricAlgorithm = "AES/CFB/PKCS5Padding"; + private static String symmetricKeyAlgorithm = "AES"; + private static int symmetricKeySize = 256; + + String keyAlias = "aced6829-63bb-5b28-8898-64efd90a70fa"; + private static String secretKey; + + static { + KeyGenerator keyGenerator = null; + try { + keyGenerator = KeyGenerator.getInstance(symmetricKeyAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + keyGenerator.init(symmetricKeySize); + secretKey = IdentityProviderUtil.b64Encode(keyGenerator.generateKey().getEncoded()); + } + + @Before + public void setUp() { + when(cacheUtilService.getSecretKey(Mockito.anyString())).thenReturn(secretKey).thenReturn(secretKey); + + ReflectionTestUtils.setField(cryptoHelper, "symmetricAlgorithm", symmetricAlgorithm); + ReflectionTestUtils.setField(cryptoHelper, "symmetricKeyAlgorithm", symmetricKeyAlgorithm); + ReflectionTestUtils.setField(cryptoHelper, "symmetricKeySize", symmetricKeySize); + } + + @Test + public void symmetricEncrypt_withValidInput_thenPass() { + String data = "test data test fatata"; + String encryptedData = cryptoHelper.symmetricEncrypt(data); + + assertNotNull(encryptedData); + verify(cacheUtilService, times(1)).getActiveKeyAlias(); + verify(cacheUtilService, times(1)).getSecretKey(keyAlias); + + String decryptedData = cryptoHelper.symmetricDecrypt(encryptedData); + assertNotNull(decryptedData); + assertEquals(data, decryptedData); + verify(cacheUtilService, times(1)).getActiveKeyAlias(); + verify(cacheUtilService, times(2)).getSecretKey(keyAlias); + } +} + diff --git a/signup-service/src/test/java/io/mosip/signup/helper/NotificationHelperTest.java b/signup-service/src/test/java/io/mosip/signup/helper/NotificationHelperTest.java new file mode 100644 index 00000000..9af18c09 --- /dev/null +++ b/signup-service/src/test/java/io/mosip/signup/helper/NotificationHelperTest.java @@ -0,0 +1,147 @@ +package io.mosip.signup.helper; + +import io.mosip.signup.dto.NotificationResponse; +import io.mosip.signup.dto.RestResponseWrapper; +import io.mosip.signup.exception.SignUpException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.*; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class NotificationHelperTest { + + @InjectMocks + private NotificationHelper notificationHelper; + + @Mock + private RestTemplate selfTokenRestTemplate; + + @Mock + private Environment environment; + + private String sendNotificationEndpoint = "http://test.endpoint.com/send-notification"; + private String defaultLanguage = "en"; + private List encodedLangCodes = List.of("es"); + + @Before + public void setUp() { + ReflectionTestUtils.setField(notificationHelper, "sendNotificationEndpoint", sendNotificationEndpoint); + ReflectionTestUtils.setField(notificationHelper, "defaultLanguage", defaultLanguage); + ReflectionTestUtils.setField(notificationHelper, "encodedLangCodes", encodedLangCodes); + } + + @Test + public void testSendSMSNotification_withValidInput_thenPass() { + String locale = "eng"; + String templateKey = "mosip.signup.sms-notification-template.send-otp"; + String message = "Hello, {{name}}!"; + + when(environment.getProperty(templateKey + "." + locale)).thenReturn(Base64.getEncoder().encodeToString(message.getBytes())); + Map params = new HashMap<>(); + params.put("{{name}}", "John"); + + RestResponseWrapper responseWrapper = new RestResponseWrapper<>(); + ResponseEntity> responseEntity = mock(ResponseEntity.class); + when(responseEntity.getBody()).thenReturn(responseWrapper); + when(selfTokenRestTemplate.exchange( + eq(sendNotificationEndpoint), + eq(HttpMethod.POST), + any(HttpEntity.class), + any(ParameterizedTypeReference.class))) + .thenReturn(responseEntity); + + notificationHelper.sendSMSNotification("1234567890", locale, templateKey, params); + + verify(selfTokenRestTemplate, times(1)).exchange( + eq(sendNotificationEndpoint), + eq(HttpMethod.POST), + any(HttpEntity.class), + any(ParameterizedTypeReference.class)); + } + + @Test(expected = SignUpException.class) + public void testSendSMSNotification_onRestException_thenFail() { + String locale = "eng"; + String templateKey = "mosip.signup.sms-notification-template.send-otp"; + String message = "Hello, {{name}}!"; + + when(environment.getProperty(templateKey + "." + locale)).thenReturn(message); + + when(selfTokenRestTemplate.exchange( + eq(sendNotificationEndpoint), + eq(HttpMethod.POST), + any(HttpEntity.class), + any(ParameterizedTypeReference.class))) + .thenThrow(new RestClientException("Error in RestTemplate")); + + notificationHelper.sendSMSNotification("1234567890", locale, templateKey, null); + } + + @Test + public void testSendSMSNotification_withNullLocale_thenPass() { //fallback to default language + String locale = null; + String templateKey = "mosip.signup.sms-notification-template.send-otp"; + String message = "Hello, {{name}}!"; + + when(environment.getProperty(templateKey + "." + defaultLanguage)).thenReturn(Base64.getEncoder().encodeToString(message.getBytes())); + + Map params = new HashMap<>(); + params.put("{{name}}", "John"); + + RestResponseWrapper responseWrapper = new RestResponseWrapper<>(); + ResponseEntity> responseEntity = mock(ResponseEntity.class); + when(responseEntity.getBody()).thenReturn(responseWrapper); + when(selfTokenRestTemplate.exchange( + eq(sendNotificationEndpoint), + eq(HttpMethod.POST), + any(HttpEntity.class), + any(ParameterizedTypeReference.class))) + .thenReturn(responseEntity); + + notificationHelper.sendSMSNotification("1234567890", locale, templateKey, params); + + verify(selfTokenRestTemplate, times(1)).exchange( + eq(sendNotificationEndpoint), + eq(HttpMethod.POST), + any(HttpEntity.class), + any(ParameterizedTypeReference.class)); + } + + @Test + public void testSendSMSNotificationAsync() { + // Verify that the async method simply delegates to the sync method + NotificationHelper spyNotificationHelper = spy(notificationHelper); + + RestResponseWrapper responseWrapper = new RestResponseWrapper<>(); + ResponseEntity> responseEntity = mock(ResponseEntity.class); + when(responseEntity.getBody()).thenReturn(responseWrapper); + when(selfTokenRestTemplate.exchange( + eq(sendNotificationEndpoint), + eq(HttpMethod.POST), + any(HttpEntity.class), + any(ParameterizedTypeReference.class))) + .thenReturn(responseEntity); + + spyNotificationHelper.sendSMSNotificationAsync("1234567890", "en", "sms.templateKey", null); + verify(spyNotificationHelper, times(1)).sendSMSNotification("1234567890", "en", "sms.templateKey", null); + } +} +