diff --git a/account-service/src/main/java/finos/traderx/accountservice/controller/HealthCheckController.java b/account-service/src/main/java/finos/traderx/accountservice/controller/HealthCheckController.java new file mode 100644 index 00000000..f5d883f0 --- /dev/null +++ b/account-service/src/main/java/finos/traderx/accountservice/controller/HealthCheckController.java @@ -0,0 +1,38 @@ +package finos.traderx.accountservice.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.jdbc.core.JdbcTemplate; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class HealthCheckController { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @GetMapping("/health") + public ResponseEntity> healthCheck() { + Map response = new HashMap<>(); + response.put("service", "account-service"); + response.put("status", "UP"); + + try { + // Test database connection + jdbcTemplate.queryForObject("SELECT 1", Integer.class); + response.put("database", "UP"); + } catch (Exception e) { + response.put("database", "DOWN"); + response.put("error", e.getMessage()); + return ResponseEntity.status(503).body(response); + } + + // Add additional health metrics + response.put("timestamp", System.currentTimeMillis()); + + return ResponseEntity.ok(response); + } +} diff --git a/account-service/src/test/java/finos/traderx/accountservice/controller/HealthCheckControllerTest.java b/account-service/src/test/java/finos/traderx/accountservice/controller/HealthCheckControllerTest.java new file mode 100644 index 00000000..224bb8dc --- /dev/null +++ b/account-service/src/test/java/finos/traderx/accountservice/controller/HealthCheckControllerTest.java @@ -0,0 +1,46 @@ +package finos.traderx.accountservice.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.mockito.Mockito.*; + +@WebMvcTest(HealthCheckController.class) +public class HealthCheckControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private JdbcTemplate jdbcTemplate; + + @Test + public void whenDatabaseIsUp_thenReturns200() throws Exception { + when(jdbcTemplate.queryForObject(eq("SELECT 1"), eq(Integer.class))).thenReturn(1); + + mockMvc.perform(MockMvcRequestBuilders.get("/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.database").value("UP")) + .andExpect(jsonPath("$.service").value("account-service")) + .andExpect(jsonPath("$.timestamp").exists()); + } + + @Test + public void whenDatabaseIsDown_thenReturns503() throws Exception { + when(jdbcTemplate.queryForObject(eq("SELECT 1"), eq(Integer.class))) + .thenThrow(new RuntimeException("Database connection failed")); + + mockMvc.perform(MockMvcRequestBuilders.get("/health")) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.database").value("DOWN")) + .andExpect(jsonPath("$.error").exists()) + .andExpect(jsonPath("$.service").value("account-service")); + } +} diff --git a/people-service/PeopleService.WebApi/Controllers/HealthController.cs b/people-service/PeopleService.WebApi/Controllers/HealthController.cs new file mode 100644 index 00000000..de4aec0c --- /dev/null +++ b/people-service/PeopleService.WebApi/Controllers/HealthController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; + +namespace PeopleService.WebApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class HealthController : ControllerBase + { + private readonly ILogger _logger; + + public HealthController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + public async Task Get() + { + try + { + var healthStatus = new + { + Service = "people-service", + Status = "UP", + Timestamp = DateTimeOffset.UtcNow, + Version = GetType().Assembly.GetName().Version?.ToString() + }; + + return Ok(healthStatus); + } + catch (Exception ex) + { + _logger.LogError(ex, "Health check failed"); + return StatusCode(500, new + { + Status = "DOWN", + Error = ex.Message, + Timestamp = DateTimeOffset.UtcNow + }); + } + } + } +} diff --git a/position-service/src/main/java/finos/traderx/positionservice/controller/HealthCheckController.java b/position-service/src/main/java/finos/traderx/positionservice/controller/HealthCheckController.java new file mode 100644 index 00000000..7fa97ab7 --- /dev/null +++ b/position-service/src/main/java/finos/traderx/positionservice/controller/HealthCheckController.java @@ -0,0 +1,38 @@ +package finos.traderx.positionservice.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.jdbc.core.JdbcTemplate; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class HealthCheckController { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @GetMapping("/health") + public ResponseEntity> healthCheck() { + Map response = new HashMap<>(); + response.put("service", "position-service"); + response.put("status", "UP"); + + try { + // Test database connection + jdbcTemplate.queryForObject("SELECT 1", Integer.class); + response.put("database", "UP"); + } catch (Exception e) { + response.put("database", "DOWN"); + response.put("error", e.getMessage()); + return ResponseEntity.status(503).body(response); + } + + // Add additional health metrics + response.put("timestamp", System.currentTimeMillis()); + + return ResponseEntity.ok(response); + } +} diff --git a/position-service/src/test/java/finos/traderx/positionservice/controller/HealthCheckControllerTest.java b/position-service/src/test/java/finos/traderx/positionservice/controller/HealthCheckControllerTest.java new file mode 100644 index 00000000..5856a873 --- /dev/null +++ b/position-service/src/test/java/finos/traderx/positionservice/controller/HealthCheckControllerTest.java @@ -0,0 +1,46 @@ +package finos.traderx.positionservice.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.mockito.Mockito.*; + +@WebMvcTest(HealthCheckController.class) +public class HealthCheckControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private JdbcTemplate jdbcTemplate; + + @Test + public void whenDatabaseIsUp_thenReturns200() throws Exception { + when(jdbcTemplate.queryForObject(eq("SELECT 1"), eq(Integer.class))).thenReturn(1); + + mockMvc.perform(MockMvcRequestBuilders.get("/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.database").value("UP")) + .andExpect(jsonPath("$.service").value("position-service")) + .andExpect(jsonPath("$.timestamp").exists()); + } + + @Test + public void whenDatabaseIsDown_thenReturns503() throws Exception { + when(jdbcTemplate.queryForObject(eq("SELECT 1"), eq(Integer.class))) + .thenThrow(new RuntimeException("Database connection failed")); + + mockMvc.perform(MockMvcRequestBuilders.get("/health")) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.database").value("DOWN")) + .andExpect(jsonPath("$.error").exists()) + .andExpect(jsonPath("$.service").value("position-service")); + } +} diff --git a/reference-data/src/health/health.controller.spec.ts b/reference-data/src/health/health.controller.spec.ts index 66c86f6f..2af29449 100644 --- a/reference-data/src/health/health.controller.spec.ts +++ b/reference-data/src/health/health.controller.spec.ts @@ -2,9 +2,11 @@ import { HealthCheckService, TerminusModule } from '@nestjs/terminus'; import { HealthCheckExecutor } from '@nestjs/terminus/dist/health-check/health-check-executor.service'; import { Test, TestingModule } from '@nestjs/testing'; import HealthController from './health.controller'; +import { HttpException } from '@nestjs/common'; describe('HealthController', () => { let controller: HealthController; + let healthService: HealthCheckService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -13,19 +15,54 @@ describe('HealthController', () => { }).compile(); controller = module.get(HealthController); + healthService = module.get(HealthCheckService); }); it('should be defined', () => { expect(controller).toBeDefined(); }); - it('Health Check', async () => { - const health = await controller.check(); - expect(health).toEqual({ + it('should return successful health check', async () => { + const mockResult = { status: 'ok', - info: {}, + info: { + referenceData: { + status: 'up', + details: { + service: 'reference-data', + timestamp: expect.any(String) + } + } + }, error: {}, - details: {} - }); + details: { + referenceData: { + status: 'up', + details: { + service: 'reference-data', + timestamp: expect.any(String) + } + } + } + }; + + const health = await controller.check(); + expect(health).toMatchObject(mockResult); + }); + + it('should handle errors appropriately', async () => { + jest.spyOn(healthService, 'check').mockRejectedValue(new Error('Test error')); + + try { + await controller.check(); + fail('Should have thrown an error'); + } catch (error) { + expect(error).toBeInstanceOf(HttpException); + expect(error.getStatus()).toBe(500); + expect(error.getResponse()).toEqual({ + status: 'error', + message: 'Test error' + }); + } }); }); \ No newline at end of file diff --git a/reference-data/src/health/health.controller.ts b/reference-data/src/health/health.controller.ts index 69618d87..0fe7dbbd 100644 --- a/reference-data/src/health/health.controller.ts +++ b/reference-data/src/health/health.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get } from '@nestjs/common'; -import { HealthCheckService, HealthCheck } from '@nestjs/terminus'; +import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common'; +import { HealthCheckService, HealthCheck, HealthCheckResult } from '@nestjs/terminus'; @Controller('health') class HealthController { @@ -7,8 +7,34 @@ class HealthController { @Get() @HealthCheck() - check() { - return this.health.check([]); + async check(): Promise { + try { + const result = await this.health.check([ + // Add specific health checks here + async () => ({ + referenceData: { + status: 'up', + details: { + service: 'reference-data', + timestamp: new Date().toISOString() + } + } + }) + ]); + + return result; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + { + status: 'error', + message: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } } } export default HealthController; \ No newline at end of file diff --git a/trade-processor/src/main/java/finos/traderx/tradeprocessor/controller/HealthCheckController.java b/trade-processor/src/main/java/finos/traderx/tradeprocessor/controller/HealthCheckController.java new file mode 100644 index 00000000..03335de6 --- /dev/null +++ b/trade-processor/src/main/java/finos/traderx/tradeprocessor/controller/HealthCheckController.java @@ -0,0 +1,38 @@ +package finos.traderx.tradeprocessor.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.jdbc.core.JdbcTemplate; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class HealthCheckController { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @GetMapping("/health") + public ResponseEntity> healthCheck() { + Map response = new HashMap<>(); + response.put("service", "trade-processor"); + response.put("status", "UP"); + + try { + // Test database connection + jdbcTemplate.queryForObject("SELECT 1", Integer.class); + response.put("database", "UP"); + } catch (Exception e) { + response.put("database", "DOWN"); + response.put("error", e.getMessage()); + return ResponseEntity.status(503).body(response); + } + + // Add additional health metrics + response.put("timestamp", System.currentTimeMillis()); + + return ResponseEntity.ok(response); + } +} diff --git a/trade-processor/src/test/java/finos/traderx/tradeprocessor/controller/HealthCheckControllerTest.java b/trade-processor/src/test/java/finos/traderx/tradeprocessor/controller/HealthCheckControllerTest.java new file mode 100644 index 00000000..68d78b49 --- /dev/null +++ b/trade-processor/src/test/java/finos/traderx/tradeprocessor/controller/HealthCheckControllerTest.java @@ -0,0 +1,46 @@ +package finos.traderx.tradeprocessor.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.mockito.Mockito.*; + +@WebMvcTest(HealthCheckController.class) +public class HealthCheckControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private JdbcTemplate jdbcTemplate; + + @Test + public void whenDatabaseIsUp_thenReturns200() throws Exception { + when(jdbcTemplate.queryForObject(eq("SELECT 1"), eq(Integer.class))).thenReturn(1); + + mockMvc.perform(MockMvcRequestBuilders.get("/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.database").value("UP")) + .andExpect(jsonPath("$.service").value("trade-processor")) + .andExpect(jsonPath("$.timestamp").exists()); + } + + @Test + public void whenDatabaseIsDown_thenReturns503() throws Exception { + when(jdbcTemplate.queryForObject(eq("SELECT 1"), eq(Integer.class))) + .thenThrow(new RuntimeException("Database connection failed")); + + mockMvc.perform(MockMvcRequestBuilders.get("/health")) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.database").value("DOWN")) + .andExpect(jsonPath("$.error").exists()) + .andExpect(jsonPath("$.service").value("trade-processor")); + } +} diff --git a/trade-service/src/main/java/finos/traderx/tradeservice/controller/HealthCheckController.java b/trade-service/src/main/java/finos/traderx/tradeservice/controller/HealthCheckController.java new file mode 100644 index 00000000..e8a5bd55 --- /dev/null +++ b/trade-service/src/main/java/finos/traderx/tradeservice/controller/HealthCheckController.java @@ -0,0 +1,38 @@ +package finos.traderx.tradeservice.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.jdbc.core.JdbcTemplate; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class HealthCheckController { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @GetMapping("/health") + public ResponseEntity> healthCheck() { + Map response = new HashMap<>(); + response.put("service", "trade-service"); + response.put("status", "UP"); + + try { + // Test database connection + jdbcTemplate.queryForObject("SELECT 1", Integer.class); + response.put("database", "UP"); + } catch (Exception e) { + response.put("database", "DOWN"); + response.put("error", e.getMessage()); + return ResponseEntity.status(503).body(response); + } + + // Add additional health metrics + response.put("timestamp", System.currentTimeMillis()); + + return ResponseEntity.ok(response); + } +} diff --git a/trade-service/src/test/java/finos/traderx/tradeservice/controller/HealthCheckControllerTest.java b/trade-service/src/test/java/finos/traderx/tradeservice/controller/HealthCheckControllerTest.java new file mode 100644 index 00000000..a503274c --- /dev/null +++ b/trade-service/src/test/java/finos/traderx/tradeservice/controller/HealthCheckControllerTest.java @@ -0,0 +1,46 @@ +package finos.traderx.tradeservice.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.mockito.Mockito.*; + +@WebMvcTest(HealthCheckController.class) +public class HealthCheckControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private JdbcTemplate jdbcTemplate; + + @Test + public void whenDatabaseIsUp_thenReturns200() throws Exception { + when(jdbcTemplate.queryForObject(eq("SELECT 1"), eq(Integer.class))).thenReturn(1); + + mockMvc.perform(MockMvcRequestBuilders.get("/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.database").value("UP")) + .andExpect(jsonPath("$.service").value("trade-service")) + .andExpect(jsonPath("$.timestamp").exists()); + } + + @Test + public void whenDatabaseIsDown_thenReturns503() throws Exception { + when(jdbcTemplate.queryForObject(eq("SELECT 1"), eq(Integer.class))) + .thenThrow(new RuntimeException("Database connection failed")); + + mockMvc.perform(MockMvcRequestBuilders.get("/health")) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.database").value("DOWN")) + .andExpect(jsonPath("$.error").exists()) + .andExpect(jsonPath("$.service").value("trade-service")); + } +}