diff --git a/trade-service/src/test/java/finos/traderx/messaging/EnvelopeInterfaceTest.java b/trade-service/src/test/java/finos/traderx/messaging/EnvelopeInterfaceTest.java new file mode 100644 index 00000000..a480b9cd --- /dev/null +++ b/trade-service/src/test/java/finos/traderx/messaging/EnvelopeInterfaceTest.java @@ -0,0 +1,123 @@ +package finos.traderx.messaging; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import java.util.Date; + +/** + * Tests for the Envelope interface contract. + * We create a minimal implementation to verify: + * 1. Message metadata handling (type, topic, from, date) + * 2. Payload handling with generics + */ +class EnvelopeInterfaceTest { + + static class TestEnvelope implements Envelope { + private final String type; + private final String topic; + private final T payload; + private final Date date; + private final String from; + + TestEnvelope(String type, String topic, T payload, Date date, String from) { + this.type = type; + this.topic = topic; + this.payload = payload; + this.date = date; + this.from = from; + } + + @Override + public String getType() { + return type; + } + + @Override + public String getTopic() { + return topic; + } + + @Override + public T getPayload() { + return payload; + } + + @Override + public Date getDate() { + return date; + } + + @Override + public String getFrom() { + return from; + } + } + + @Test + void envelope_WithStringPayload_HandlesAllFields() { + // Arrange + String type = "test-type"; + String topic = "/test/topic"; + String payload = "test payload"; + Date date = new Date(); + String from = "test-sender"; + + // Act + TestEnvelope envelope = new TestEnvelope<>(type, topic, payload, date, from); + + // Assert + assertEquals(type, envelope.getType(), "Type should match"); + assertEquals(topic, envelope.getTopic(), "Topic should match"); + assertEquals(payload, envelope.getPayload(), "Payload should match"); + assertEquals(date, envelope.getDate(), "Date should match"); + assertEquals(from, envelope.getFrom(), "Sender should match"); + } + + @Test + void envelope_WithCustomPayload_HandlesGenericType() { + // Arrange + class CustomPayload { + String value; + CustomPayload(String value) { this.value = value; } + } + + CustomPayload payload = new CustomPayload("test"); + Date now = new Date(); + + // Act + TestEnvelope envelope = new TestEnvelope<>( + "custom", "/test", payload, now, "sender"); + + // Assert + assertSame(payload, envelope.getPayload(), + "Should handle custom payload type"); + assertEquals("test", envelope.getPayload().value, + "Should preserve payload data"); + } + + @Test + void envelope_WithNullPayload_HandlesNull() { + // Arrange & Act + TestEnvelope envelope = new TestEnvelope<>( + "null-test", "/test", null, new Date(), "sender"); + + // Assert + assertNull(envelope.getPayload(), + "Should handle null payload"); + } + + @Test + void envelope_DateField_ReturnsOriginalDate() { + // Arrange + Date date = new Date(); + TestEnvelope envelope = new TestEnvelope<>( + "test", "/test", "payload", date, "sender"); + + // Act + Date returnedDate = envelope.getDate(); + + // Assert + assertSame(date, returnedDate, + "Should return the exact Date instance provided"); + } +} diff --git a/trade-service/src/test/java/finos/traderx/messaging/PublisherInterfaceTest.java b/trade-service/src/test/java/finos/traderx/messaging/PublisherInterfaceTest.java new file mode 100644 index 00000000..c211a196 --- /dev/null +++ b/trade-service/src/test/java/finos/traderx/messaging/PublisherInterfaceTest.java @@ -0,0 +1,135 @@ +package finos.traderx.messaging; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + +/** + * Tests for the Publisher interface contract. + * We create a minimal implementation to verify: + * 1. Basic publishing functionality (with and without topic) + * 2. Connection management + * 3. Error handling with PubSubException + */ +class PublisherInterfaceTest { + + static class TestPublisher implements Publisher { + boolean connected = false; + String lastMessage = null; + String lastTopic = null; + boolean shouldThrowOnPublish = false; + + @Override + public void publish(String message) throws PubSubException { + publish("/default", message); + } + + @Override + public void publish(String topic, String message) throws PubSubException { + if (!isConnected()) { + throw new PubSubException("Not connected"); + } + if (shouldThrowOnPublish) { + throw new PubSubException("Simulated publish error"); + } + this.lastTopic = topic; + this.lastMessage = message; + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public void connect() throws PubSubException { + if (connected) { + throw new PubSubException("Already connected"); + } + connected = true; + } + + @Override + public void disconnect() throws PubSubException { + if (!connected) { + throw new PubSubException("Already disconnected"); + } + connected = false; + } + } + + @Test + void publish_WhenConnected_PublishesMessage() throws PubSubException { + // Arrange + TestPublisher publisher = new TestPublisher(); + publisher.connect(); + String message = "test message"; + String topic = "/test/topic"; + + // Act + publisher.publish(topic, message); + + // Assert + assertEquals(message, publisher.lastMessage, "Message should be stored"); + assertEquals(topic, publisher.lastTopic, "Topic should be stored"); + } + + @Test + void publish_WhenDisconnected_ThrowsPubSubException() { + // Arrange + TestPublisher publisher = new TestPublisher(); + String message = "test message"; + + // Act & Assert + assertThrows(PubSubException.class, () -> publisher.publish(message), + "Publishing while disconnected should throw PubSubException"); + } + + @Test + void connect_WhenAlreadyConnected_ThrowsPubSubException() throws PubSubException { + // Arrange + TestPublisher publisher = new TestPublisher(); + publisher.connect(); + + // Act & Assert + assertThrows(PubSubException.class, () -> publisher.connect(), + "Connecting when already connected should throw PubSubException"); + } + + @Test + void disconnect_WhenNotConnected_ThrowsPubSubException() { + // Arrange + TestPublisher publisher = new TestPublisher(); + + // Act & Assert + assertThrows(PubSubException.class, () -> publisher.disconnect(), + "Disconnecting when not connected should throw PubSubException"); + } + + @Test + void connectionLifecycle_WorksCorrectly() throws PubSubException { + // Arrange + TestPublisher publisher = new TestPublisher(); + + // Act & Assert - Connection cycle + assertFalse(publisher.isConnected(), "Should start disconnected"); + + publisher.connect(); + assertTrue(publisher.isConnected(), "Should be connected after connect()"); + + publisher.disconnect(); + assertFalse(publisher.isConnected(), "Should be disconnected after disconnect()"); + } + + @Test + void publish_WithError_ThrowsPubSubException() throws PubSubException { + // Arrange + TestPublisher publisher = new TestPublisher(); + publisher.connect(); + publisher.shouldThrowOnPublish = true; + + // Act & Assert + assertThrows(PubSubException.class, + () -> publisher.publish("test message"), + "Should throw PubSubException when publish fails"); + } +} diff --git a/trade-service/src/test/java/finos/traderx/messaging/SubscriberInterfaceTest.java b/trade-service/src/test/java/finos/traderx/messaging/SubscriberInterfaceTest.java new file mode 100644 index 00000000..28f8eb1b --- /dev/null +++ b/trade-service/src/test/java/finos/traderx/messaging/SubscriberInterfaceTest.java @@ -0,0 +1,175 @@ +package finos.traderx.messaging; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + +/** + * Tests for the Subscriber interface contract. + * We create a minimal implementation to verify: + * 1. Basic subscription functionality + * 2. Message handling + * 3. Connection management + * 4. Error handling with PubSubException + */ +class SubscriberInterfaceTest { + + static class TestSubscriber implements Subscriber { + boolean connected = false; + String lastTopic = null; + String lastMessage = null; + Envelope lastEnvelope = null; + boolean shouldThrowOnSubscribe = false; + + @Override + public void subscribe(String topic) throws PubSubException { + if (!isConnected()) { + throw new PubSubException("Not connected"); + } + if (shouldThrowOnSubscribe) { + throw new PubSubException("Simulated subscribe error"); + } + this.lastTopic = topic; + } + + @Override + public void unsubscribe(String topic) throws PubSubException { + if (!isConnected()) { + throw new PubSubException("Not connected"); + } + if (lastTopic != null && lastTopic.equals(topic)) { + lastTopic = null; + } else { + throw new PubSubException("Not subscribed to topic: " + topic); + } + } + + @Override + public void onMessage(Envelope envelope, String message) { + this.lastEnvelope = envelope; + this.lastMessage = message; + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public void connect() throws PubSubException { + if (connected) { + throw new PubSubException("Already connected"); + } + connected = true; + } + + @Override + public void disconnect() throws PubSubException { + if (!connected) { + throw new PubSubException("Already disconnected"); + } + connected = false; + lastTopic = null; + } + } + + @Test + void subscribe_WhenConnected_SubscribesToTopic() throws PubSubException { + // Arrange + TestSubscriber subscriber = new TestSubscriber(); + subscriber.connect(); + String topic = "/test/topic"; + + // Act + subscriber.subscribe(topic); + + // Assert + assertEquals(topic, subscriber.lastTopic, "Should be subscribed to the topic"); + } + + @Test + void subscribe_WhenDisconnected_ThrowsPubSubException() { + // Arrange + TestSubscriber subscriber = new TestSubscriber(); + String topic = "/test/topic"; + + // Act & Assert + assertThrows(PubSubException.class, () -> subscriber.subscribe(topic), + "Subscribing while disconnected should throw PubSubException"); + } + + @Test + void unsubscribe_FromSubscribedTopic_Succeeds() throws PubSubException { + // Arrange + TestSubscriber subscriber = new TestSubscriber(); + subscriber.connect(); + String topic = "/test/topic"; + subscriber.subscribe(topic); + + // Act + subscriber.unsubscribe(topic); + + // Assert + assertNull(subscriber.lastTopic, "Should be unsubscribed from the topic"); + } + + @Test + void unsubscribe_FromUnsubscribedTopic_ThrowsPubSubException() throws PubSubException { + // Arrange + TestSubscriber subscriber = new TestSubscriber(); + subscriber.connect(); + + // Act & Assert + assertThrows(PubSubException.class, + () -> subscriber.unsubscribe("/nonexistent"), + "Unsubscribing from unsubscribed topic should throw PubSubException"); + } + + @Test + void onMessage_HandlesMessageAndEnvelope() { + // Arrange + TestSubscriber subscriber = new TestSubscriber(); + String message = "test message"; + TestEnvelope envelope = new TestEnvelope(); + + // Act + subscriber.onMessage(envelope, message); + + // Assert + assertEquals(message, subscriber.lastMessage, "Message should be stored"); + assertEquals(envelope, subscriber.lastEnvelope, "Envelope should be stored"); + } + + @Test + void connectionLifecycle_WorksCorrectly() throws PubSubException { + // Arrange + TestSubscriber subscriber = new TestSubscriber(); + + // Act & Assert - Connection cycle + assertFalse(subscriber.isConnected(), "Should start disconnected"); + + subscriber.connect(); + assertTrue(subscriber.isConnected(), "Should be connected after connect()"); + + subscriber.disconnect(); + assertFalse(subscriber.isConnected(), "Should be disconnected after disconnect()"); + assertNull(subscriber.lastTopic, "Should clear subscriptions on disconnect"); + } + + // Helper class for testing + static class TestEnvelope implements Envelope { + @Override + public String getType() { return "test"; } + + @Override + public String getTopic() { return "/test"; } + + @Override + public String getPayload() { return "test payload"; } + + @Override + public java.util.Date getDate() { return new java.util.Date(); } + + @Override + public String getFrom() { return "test-sender"; } + } +} diff --git a/trade-service/src/test/java/finos/traderx/tradeservice/controller/DocsControllerTest.java b/trade-service/src/test/java/finos/traderx/tradeservice/controller/DocsControllerTest.java new file mode 100644 index 00000000..2b73e891 --- /dev/null +++ b/trade-service/src/test/java/finos/traderx/tradeservice/controller/DocsControllerTest.java @@ -0,0 +1,25 @@ +package finos.traderx.tradeservice.controller; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + +/** + * Tests for DocsController that verify: + * 1. Root URL redirects to Swagger UI + * This is a minimal controller that only handles redirects, so testing is straightforward + */ +class DocsControllerTest { + + @Test + void index_ReturnsSwaggerRedirect() { + // Arrange + DocsController controller = new DocsController(); + + // Act + String viewName = controller.index(); + + // Assert + assertEquals("redirect:swagger-ui.html", viewName, + "Root URL should redirect to Swagger UI"); + } +} diff --git a/trade-service/src/test/java/finos/traderx/tradeservice/controller/TradeOrderControllerTest.java b/trade-service/src/test/java/finos/traderx/tradeservice/controller/TradeOrderControllerTest.java new file mode 100644 index 00000000..313cb1a1 --- /dev/null +++ b/trade-service/src/test/java/finos/traderx/tradeservice/controller/TradeOrderControllerTest.java @@ -0,0 +1,115 @@ +package finos.traderx.tradeservice.controller; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +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 org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import finos.traderx.messaging.Publisher; +import finos.traderx.messaging.PubSubException; +import finos.traderx.tradeservice.exceptions.ResourceNotFoundException; +import finos.traderx.tradeservice.model.*; + +/** + * Tests for TradeOrderController that focus on: + * 1. Trade order validation logic (security and account validation) + * 2. Integration with external services via RestTemplate + * 3. Publishing trade orders to the message bus + * 4. Error handling for various scenarios + */ +class TradeOrderControllerTest { + + @Mock + private Publisher tradePublisher; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private TradeOrderController controller; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(controller, "referenceDataServiceAddress", "http://reference-data:8080"); + ReflectionTestUtils.setField(controller, "accountServiceAddress", "http://account-service:8080"); + ReflectionTestUtils.setField(controller, "restTemplate", restTemplate); + } + + @Test + void createTradeOrder_ValidOrder_SuccessfullyPublished() throws PubSubException { + // Arrange + TradeOrder order = new TradeOrder("123", 456, "AAPL", TradeSide.Buy, 100); + + // Mock successful validation responses + when(restTemplate.getForEntity(anyString(), eq(Security.class))) + .thenReturn(ResponseEntity.ok(new Security())); + when(restTemplate.getForEntity(anyString(), eq(Account.class))) + .thenReturn(ResponseEntity.ok(new Account())); + + // Act + ResponseEntity response = controller.createTradeOrder(order); + + // Assert + assertNotNull(response); + assertEquals(200, response.getStatusCode().value()); + assertEquals(order, response.getBody()); + verify(tradePublisher).publish("/trades", order); + } + + @Test + void createTradeOrder_InvalidSecurity_ThrowsResourceNotFoundException() throws PubSubException { + // Arrange + TradeOrder order = new TradeOrder("123", 456, "INVALID", TradeSide.Buy, 100); + + // Mock 404 response for security validation + when(restTemplate.getForEntity(contains("/stocks/"), eq(Security.class))) + .thenThrow(HttpClientErrorException.NotFound.class); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> controller.createTradeOrder(order)); + verify(tradePublisher, never()).publish(anyString(), any()); + } + + @Test + void createTradeOrder_InvalidAccount_ThrowsResourceNotFoundException() throws PubSubException { + // Arrange + TradeOrder order = new TradeOrder("123", 999, "AAPL", TradeSide.Buy, 100); + + // Mock successful security validation but failed account validation + when(restTemplate.getForEntity(contains("/stocks/"), eq(Security.class))) + .thenReturn(ResponseEntity.ok(new Security())); + when(restTemplate.getForEntity(contains("/account/"), eq(Account.class))) + .thenThrow(HttpClientErrorException.NotFound.class); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> controller.createTradeOrder(order)); + verify(tradePublisher, never()).publish(anyString(), any()); + } + + @Test + void createTradeOrder_PublishError_ThrowsRuntimeException() throws PubSubException { + // Arrange + TradeOrder order = new TradeOrder("123", 456, "AAPL", TradeSide.Buy, 100); + + // Mock successful validations but failed publish + when(restTemplate.getForEntity(anyString(), eq(Security.class))) + .thenReturn(ResponseEntity.ok(new Security())); + when(restTemplate.getForEntity(anyString(), eq(Account.class))) + .thenReturn(ResponseEntity.ok(new Account())); + doThrow(new PubSubException("Failed to publish")) + .when(tradePublisher).publish(anyString(), any()); + + // Act & Assert + assertThrows(RuntimeException.class, () -> controller.createTradeOrder(order)); + } +} diff --git a/trade-service/src/test/java/finos/traderx/tradeservice/exceptions/ResourceNotFoundExceptionTest.java b/trade-service/src/test/java/finos/traderx/tradeservice/exceptions/ResourceNotFoundExceptionTest.java new file mode 100644 index 00000000..283d253d --- /dev/null +++ b/trade-service/src/test/java/finos/traderx/tradeservice/exceptions/ResourceNotFoundExceptionTest.java @@ -0,0 +1,44 @@ +package finos.traderx.tradeservice.exceptions; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Tests for ResourceNotFoundException that verify: + * 1. Exception message handling + * 2. HTTP status annotation is present and correct + * 3. Exception inherits from RuntimeException + */ +class ResourceNotFoundExceptionTest { + + @Test + void constructor_SetsMessage() { + // Arrange + String message = "Resource XYZ not found"; + + // Act + ResourceNotFoundException ex = new ResourceNotFoundException(message); + + // Assert + assertEquals(message, ex.getMessage(), + "Exception should store and return the provided message"); + } + + @Test + void class_HasCorrectAnnotation() { + // Verify @ResponseStatus annotation is present with NOT_FOUND + ResponseStatus annotation = ResourceNotFoundException.class.getAnnotation(ResponseStatus.class); + assertNotNull(annotation, "Class should have @ResponseStatus annotation"); + assertEquals(HttpStatus.NOT_FOUND, annotation.value(), + "Annotation should specify HTTP 404 NOT_FOUND status"); + } + + @Test + void class_InheritsFromRuntimeException() { + // Verify exception hierarchy + assertTrue(RuntimeException.class.isAssignableFrom(ResourceNotFoundException.class), + "Should inherit from RuntimeException for unchecked exception behavior"); + } +}