diff --git a/pom.xml b/pom.xml index 1dcc6441..9411119c 100644 --- a/pom.xml +++ b/pom.xml @@ -125,10 +125,6 @@ com.vaadin.external.google android-json - - com.jayway.jsonpath - json-path - @@ -285,6 +281,17 @@ h2 runtime + + com.jayway.jsonpath + json-path + 2.9.0 + test + + + org.mockito + mockito-junit-jupiter + test + ${artifactId}-${version} diff --git a/src/test/java/com/iemr/inventory/RoleMasterApplicationTest.java b/src/test/java/com/iemr/inventory/RoleMasterApplicationTest.java new file mode 100644 index 00000000..2c99b90d --- /dev/null +++ b/src/test/java/com/iemr/inventory/RoleMasterApplicationTest.java @@ -0,0 +1,100 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +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 org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import com.iemr.inventory.utils.IEMRApplBeans; + +@ExtendWith(MockitoExtension.class) +@DisplayName("RoleMasterApplication Test Suite") +class RoleMasterApplicationTest { + + @Mock + private RedisConnectionFactory connectionFactory; + + private final RoleMasterApplication application = new RoleMasterApplication(); + + @Test + @DisplayName("configProperties should supply a fresh properties holder on each call") + void configProperties_shouldSupplyFreshHolder() { + assertNotNull(application.configProperties()); + assertNotSame(application.configProperties(), application.configProperties()); + } + + @Test + @DisplayName("instantiateBeans should supply the shared bean configuration") + void instantiateBeans_shouldSupplyBeanConfiguration() { + assertInstanceOf(IEMRApplBeans.class, application.instantiateBeans()); + } + + @Test + @DisplayName("configure should point the servlet container at this application class") + void configure_shouldPointAtApplicationClass() { + SpringApplicationBuilder builder = new SpringApplicationBuilder(); + + assertSame(builder, application.configure(builder)); + } + + @Test + @DisplayName("redisTemplate should bind the connection factory and serialise keys as plain strings") + void redisTemplate_shouldBindFactoryAndConfigureSerializers() { + RedisTemplate template = application.redisTemplate(connectionFactory); + + assertSame(connectionFactory, template.getConnectionFactory()); + assertInstanceOf(StringRedisSerializer.class, template.getKeySerializer()); + assertInstanceOf(Jackson2JsonRedisSerializer.class, template.getValueSerializer()); + } + + @Test + @DisplayName("ServletInitializer should point the WAR deployment at the same application class") + void servletInitializer_shouldPointAtApplicationClass() { + SpringApplicationBuilder builder = new SpringApplicationBuilder(); + + assertSame(builder, new ServletInitializer().configure(builder)); + } + + @Test + @DisplayName("ServletInitializer onStartup should complete without touching the servlet context") + void servletInitializer_onStartupShouldCompleteQuietly() throws Exception { + jakarta.servlet.ServletContext servletContext = + org.mockito.Mockito.mock(jakarta.servlet.ServletContext.class); + + new ServletInitializer().onStartup(servletContext); + + org.mockito.Mockito.verifyNoInteractions(servletContext); + } +} diff --git a/src/test/java/com/iemr/inventory/config/HttpInterceptorConfigTest.java b/src/test/java/com/iemr/inventory/config/HttpInterceptorConfigTest.java new file mode 100644 index 00000000..496bff6f --- /dev/null +++ b/src/test/java/com/iemr/inventory/config/HttpInterceptorConfigTest.java @@ -0,0 +1,77 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.config; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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 org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.servlet.config.annotation.InterceptorRegistration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; + +import com.iemr.inventory.utils.http.HTTPRequestInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@ExtendWith(MockitoExtension.class) +@DisplayName("Web MVC configuration Test Suite") +class HttpInterceptorConfigTest { + + @Mock + private HTTPRequestInterceptor httpInterceptor; + @Mock + private InterceptorRegistry registry; + @Mock + private InterceptorRegistration registration; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + + @Test + @DisplayName("addInterceptors should register the session-checking interceptor") + void addInterceptors_shouldRegisterSessionInterceptor() { + HttpInterceptorConfig config = new HttpInterceptorConfig(); + ReflectionTestUtils.setField(config, "httpInterceptor", httpInterceptor); + when(registry.addInterceptor(httpInterceptor)).thenReturn(registration); + + config.addInterceptors(registry); + + verify(registry).addInterceptor(httpInterceptor); + } + + @Test + @DisplayName("BlockingMethodInterceptor should complete its post-handle and after-completion hooks quietly") + void blockingMethodInterceptor_shouldCompleteHooksQuietly() { + BlockingMethodInterceptor interceptor = new BlockingMethodInterceptor(); + + assertDoesNotThrow(() -> interceptor.postHandle(request, response, new Object(), null)); + assertDoesNotThrow(() -> interceptor.afterCompletion(request, response, new Object(), null)); + } +} diff --git a/src/test/java/com/iemr/inventory/config/RedisConfigTest.java b/src/test/java/com/iemr/inventory/config/RedisConfigTest.java new file mode 100644 index 00000000..ce9865f6 --- /dev/null +++ b/src/test/java/com/iemr/inventory/config/RedisConfigTest.java @@ -0,0 +1,88 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +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 org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.inventory.data.user.M_User; + +@ExtendWith(MockitoExtension.class) +@DisplayName("RedisConfig Test Suite") +class RedisConfigTest { + + @Mock + private RedisConnectionFactory connectionFactory; + + private RedisConfig redisConfig; + + @BeforeEach + @DisplayName("Create the configuration with Redis coordinates before each test") + void setUp() { + redisConfig = new RedisConfig(); + ReflectionTestUtils.setField(redisConfig, "redisHost", "redis.example.org"); + ReflectionTestUtils.setField(redisConfig, "redisPort", 6379); + } + + @Test + @DisplayName("lettuceConnectionFactory should point Lettuce at the configured host and port") + void lettuceConnectionFactory_shouldUseConfiguredHostAndPort() { + LettuceConnectionFactory factory = redisConfig.lettuceConnectionFactory(); + + assertNotNull(factory); + assertEquals("redis.example.org", factory.getHostName()); + assertEquals(6379, factory.getPort()); + } + + @Test + @DisplayName("redisTemplate should bind the supplied connection factory") + void redisTemplate_shouldBindSuppliedConnectionFactory() { + RedisTemplate template = redisConfig.redisTemplate(connectionFactory); + + assertNotNull(template); + assertSame(connectionFactory, template.getConnectionFactory()); + } + + @Test + @DisplayName("redisTemplate should serialise keys as plain strings and values as M_User JSON") + void redisTemplate_shouldConfigureSerializers() { + RedisTemplate template = redisConfig.redisTemplate(connectionFactory); + + assertInstanceOf(StringRedisSerializer.class, template.getKeySerializer()); + assertInstanceOf(Jackson2JsonRedisSerializer.class, template.getValueSerializer()); + } +} diff --git a/src/test/java/com/iemr/inventory/config/SwaggerConfigTest.java b/src/test/java/com/iemr/inventory/config/SwaggerConfigTest.java new file mode 100644 index 00000000..05c0e341 --- /dev/null +++ b/src/test/java/com/iemr/inventory/config/SwaggerConfigTest.java @@ -0,0 +1,101 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.env.MockEnvironment; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityScheme; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("SwaggerConfig Test Suite") +class SwaggerConfigTest { + + private static final String SECURITY_SCHEME_NAME = "bearerAuth"; + private static final String DEFAULT_URL = "http://localhost:9090"; + + private SwaggerConfig swaggerConfig; + private MockEnvironment environment; + + @BeforeEach + @DisplayName("Create the configuration and an empty environment before each test") + void setUp() { + swaggerConfig = new SwaggerConfig(); + environment = new MockEnvironment(); + } + + @Test + @DisplayName("customOpenAPI should describe the inventory API") + void customOpenAPI_shouldDescribeTheApi() { + OpenAPI openAPI = swaggerConfig.customOpenAPI(environment); + + assertNotNull(openAPI.getInfo()); + assertEquals("Inventory API", openAPI.getInfo().getTitle()); + assertEquals("version", openAPI.getInfo().getVersion()); + assertTrue(openAPI.getInfo().getDescription().contains("inventory management")); + } + + @Test + @DisplayName("customOpenAPI should declare a bearer security scheme and require it") + void customOpenAPI_shouldDeclareBearerSecurityScheme() { + OpenAPI openAPI = swaggerConfig.customOpenAPI(environment); + + SecurityScheme scheme = openAPI.getComponents().getSecuritySchemes().get(SECURITY_SCHEME_NAME); + assertNotNull(scheme); + assertEquals(SecurityScheme.Type.HTTP, scheme.getType()); + assertEquals("bearer", scheme.getScheme()); + assertEquals(1, openAPI.getSecurity().size()); + assertTrue(openAPI.getSecurity().get(0).containsKey(SECURITY_SCHEME_NAME)); + } + + @Test + @DisplayName("customOpenAPI should fall back to localhost for every unset server url") + void customOpenAPI_shouldFallBackToLocalhostForUnsetUrls() { + OpenAPI openAPI = swaggerConfig.customOpenAPI(environment); + + assertEquals(3, openAPI.getServers().size()); + openAPI.getServers().forEach(server -> assertEquals(DEFAULT_URL, server.getUrl())); + assertEquals("Dev", openAPI.getServers().get(0).getDescription()); + assertEquals("UAT", openAPI.getServers().get(1).getDescription()); + assertEquals("Demo", openAPI.getServers().get(2).getDescription()); + } + + @Test + @DisplayName("customOpenAPI should use the configured dev, UAT and demo urls when present") + void customOpenAPI_shouldUseConfiguredUrls() { + environment.setProperty("api.dev.url", "https://dev.amrit.example.org"); + environment.setProperty("api.uat.url", "https://uat.amrit.example.org"); + environment.setProperty("api.demo.url", "https://demo.amrit.example.org"); + + OpenAPI openAPI = swaggerConfig.customOpenAPI(environment); + + assertEquals("https://dev.amrit.example.org", openAPI.getServers().get(0).getUrl()); + assertEquals("https://uat.amrit.example.org", openAPI.getServers().get(1).getUrl()); + assertEquals("https://demo.amrit.example.org", openAPI.getServers().get(2).getUrl()); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/Supplier/SupplierMasterControllerTest.java b/src/test/java/com/iemr/inventory/controller/Supplier/SupplierMasterControllerTest.java new file mode 100644 index 00000000..b43076fa --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/Supplier/SupplierMasterControllerTest.java @@ -0,0 +1,220 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.Supplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.supplier.M_Supplier; +import com.iemr.inventory.data.supplier.M_Supplieraddress; +import com.iemr.inventory.service.supplier.SupplierInter; + +@ExtendWith(MockitoExtension.class) +@DisplayName("SupplierMasterController Test Suite") +class SupplierMasterControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private SupplierInter supplierInter; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked supplier service") + void setUp() { + SupplierMasterController controller = new SupplierMasterController(); + ReflectionTestUtils.setField(controller, "supplierInter", supplierInter); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static M_Supplier supplier(Integer id, String name) { + M_Supplier supplier = new M_Supplier(); + supplier.setSupplierID(id); + supplier.setSupplierName(name); + supplier.setProviderServiceMapID(3); + return supplier; + } + + @Test + @DisplayName("createSupplier should persist the supplier and derive its address row from the same payload") + void createSupplier_shouldPersistSupplierAndAddress() throws Exception { + when(supplierInter.createSupplier(anyList())).thenReturn(new ArrayList<>(List.of(supplier(11, "Acme")))); + when(supplierInter.createAddress(anyList())).thenReturn(new ArrayList<>()); + + mockMvc.perform(post("/createSupplier").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{\"supplierName\":\"Acme\",\"providerServiceMapID\":3,\"addressLine1\":\"12 Mill Road\"," + + "\"addressLine2\":\"Suite 4\",\"district\":\"Pune\",\"state\":\"MH\",\"country\":\"IN\"," + + "\"pinCode\":\"411001\",\"createdBy\":\"tester\"}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].supplierName").value("Acme")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(supplierInter).createAddress(captor.capture()); + M_Supplieraddress address = captor.getValue().get(0); + assertEquals(11, address.getSupplierID()); + assertEquals("12 Mill Road", address.getAddressLine1()); + assertEquals("Pune", address.getDistrict()); + assertEquals("411001", address.getPinCode()); + assertEquals("tester", address.getCreatedBy()); + } + + @Test + @DisplayName("createSupplier should report the failure when the service blows up") + void createSupplier_shouldReportServiceFailure() throws Exception { + when(supplierInter.createSupplier(anyList())).thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/createSupplier").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("db unavailable")); + } + + @Test + @DisplayName("createSupplier should report the failure when the supplier save returns nothing to map") + void createSupplier_shouldReportFailureWhenNothingSaved() throws Exception { + when(supplierInter.createSupplier(anyList())).thenReturn(null); + + mockMvc.perform(post("/createSupplier").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{\"supplierName\":\"Acme\"}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("getSupplier should look the rows up by the posted provider service map id") + void getSupplier_shouldLookUpByProviderServiceMapId() throws Exception { + when(supplierInter.getSupplier(3)) + .thenReturn(new ArrayList<>(List.of(supplier(11, "Acme"), supplier(12, "Beta")))); + + mockMvc.perform(post("/getSupplier").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.length()").value(2)); + + verify(supplierInter).getSupplier(3); + } + + @Test + @DisplayName("getSupplier should report the failure when the lookup throws") + void getSupplier_shouldReportServiceFailure() throws Exception { + when(supplierInter.getSupplier(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getSupplier").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("editSupplier should copy every editable field onto the stored row before saving") + void editSupplier_shouldCopyEditableFieldsBeforeSaving() throws Exception { + M_Supplier stored = supplier(11, "old name"); + when(supplierInter.editSupplier(11)).thenReturn(stored); + when(supplierInter.saveEditedData(any(M_Supplier.class))).thenAnswer(inv -> inv.getArgument(0)); + + mockMvc.perform(post("/editSupplier").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"supplierID\":11,\"supplierName\":\"new name\",\"supplierDesc\":\"new desc\"," + + "\"supplierCode\":\"NEW\",\"status\":\"Inactive\",\"contactPerson\":\"Alex\"," + + "\"drugLicenseNo\":\"DL-1\",\"cST_GST_No\":\"GST-9\",\"tIN_No\":\"TIN-4\"," + + "\"email\":\"acme@example.org\",\"phoneNo1\":\"111\",\"phoneNo2\":\"222\"," + + "\"modifiedBy\":\"tester\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.supplierName").value("new name")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Supplier.class); + verify(supplierInter).saveEditedData(captor.capture()); + M_Supplier saved = captor.getValue(); + assertEquals("DL-1", saved.getDrugLicenseNo()); + assertEquals("GST-9", saved.getcST_GST_No()); + assertEquals("TIN-4", saved.gettIN_No()); + assertEquals("acme@example.org", saved.getEmail()); + assertEquals("111", saved.getPhoneNo1()); + assertEquals("222", saved.getPhoneNo2()); + assertEquals("tester", saved.getModifiedBy()); + } + + @Test + @DisplayName("editSupplier should report the failure when the row cannot be found") + void editSupplier_shouldReportFailureWhenRowMissing() throws Exception { + when(supplierInter.editSupplier(11)).thenReturn(null); + + mockMvc.perform(post("/editSupplier").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"supplierID\":11}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("deleteSupplier should flip the deleted flag on the stored row and save it") + void deleteSupplier_shouldFlipDeletedFlag() throws Exception { + M_Supplier stored = supplier(11, "Acme"); + when(supplierInter.editSupplier(11)).thenReturn(stored); + when(supplierInter.saveEditedData(any(M_Supplier.class))).thenAnswer(inv -> inv.getArgument(0)); + + mockMvc.perform(post("/deleteSupplier").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"supplierID\":11,\"deleted\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Supplier.class); + verify(supplierInter).saveEditedData(captor.capture()); + assertEquals(Boolean.TRUE, captor.getValue().getDeleted()); + } + + @Test + @DisplayName("deleteSupplier should report the failure when the row cannot be found") + void deleteSupplier_shouldReportFailureWhenRowMissing() throws Exception { + when(supplierInter.editSupplier(11)).thenReturn(null); + + mockMvc.perform(post("/deleteSupplier").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"supplierID\":11}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/dispenseagainst_rx/DispenseAgainstRXTest.java b/src/test/java/com/iemr/inventory/controller/dispenseagainst_rx/DispenseAgainstRXTest.java new file mode 100644 index 00000000..48c7d1da --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/dispenseagainst_rx/DispenseAgainstRXTest.java @@ -0,0 +1,103 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.dispenseagainst_rx; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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 org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.service.dispenseagainst_rx.DispenseAgainstRXimpl; + +@ExtendWith(MockitoExtension.class) +@DisplayName("DispenseAgainstRX controller Test Suite") +class DispenseAgainstRXTest { + + private static final String AUTH = "test-session-key"; + private static final String REQUEST = "{\"beneficiaryRegID\":101,\"visitCode\":5001,\"facilityID\":7}"; + + @Mock + private DispenseAgainstRXimpl dispenseAgainstRXimpl; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked dispensing service") + void setUp() { + DispenseAgainstRX controller = new DispenseAgainstRX(); + ReflectionTestUtils.setField(controller, "dispenseAgainstRXimpl", dispenseAgainstRXimpl); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + @DisplayName("getPrescribedMedicines should hand the service payload back to the caller") + void getPrescribedMedicines_shouldReturnServicePayload() throws Exception { + when(dispenseAgainstRXimpl.getPrescribedMedicines(anyString())) + .thenReturn("{\"prescriptionID\":9001}"); + + mockMvc.perform(post("/RX/getPrescribedMedicines").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(REQUEST)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.prescriptionID").value(9001)); + + verify(dispenseAgainstRXimpl).getPrescribedMedicines(REQUEST); + } + + @Test + @DisplayName("getPrescribedMedicines should leave the generic failure standing when the service returns nothing") + void getPrescribedMedicines_shouldLeaveGenericFailureWhenServiceReturnsNothing() throws Exception { + when(dispenseAgainstRXimpl.getPrescribedMedicines(anyString())).thenReturn(null); + + mockMvc.perform(post("/RX/getPrescribedMedicines").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(REQUEST)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.status").value("FAILURE")); + } + + @Test + @DisplayName("getPrescribedMedicines should report the failure when the service throws") + void getPrescribedMedicines_shouldReportServiceFailure() throws Exception { + when(dispenseAgainstRXimpl.getPrescribedMedicines(anyString())) + .thenThrow(new RuntimeException("prescription lookup failed")); + + mockMvc.perform(post("/RX/getPrescribedMedicines").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(REQUEST)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("prescription lookup failed")); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/drugtype/DrugtypeControllerTest.java b/src/test/java/com/iemr/inventory/controller/drugtype/DrugtypeControllerTest.java new file mode 100644 index 00000000..360b8cf6 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/drugtype/DrugtypeControllerTest.java @@ -0,0 +1,202 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.drugtype; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.drugtype.M_Drugtype; +import com.iemr.inventory.service.drugtype.DrugtypeInter; + +@ExtendWith(MockitoExtension.class) +@DisplayName("DrugtypeController Test Suite") +class DrugtypeControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private DrugtypeInter drugtypeInter; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked drug type service") + void setUp() { + DrugtypeController controller = new DrugtypeController(); + ReflectionTestUtils.setField(controller, "drugtypeInter", drugtypeInter); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static M_Drugtype row(Integer id, String name) { + M_Drugtype row = new M_Drugtype(); + row.setDrugTypeID(id); + row.setDrugTypeName(name); + row.setProviderServiceMapID(3); + return row; + } + + private static ArrayList rows(M_Drugtype... items) { + return new ArrayList<>(List.of(items)); + } + + @Test + @DisplayName("createManufacturer should persist the posted array and answer with the saved rows") + void createManufacturer_shouldPersistPostedArray() throws Exception { + when(drugtypeInter.createDrugtypeData(anyList())).thenReturn(rows(row(1, "first"))); + + mockMvc.perform(post("/createDrugtype").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{\"drugTypeName\":\"first\",\"providerServiceMapID\":3}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].drugTypeName").value("first")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(drugtypeInter).createDrugtypeData(captor.capture()); + assertEquals(1, captor.getValue().size()); + } + + @Test + @DisplayName("createManufacturer should report the failure when the service blows up") + void createManufacturer_shouldReportServiceFailure() throws Exception { + when(drugtypeInter.createDrugtypeData(anyList())).thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/createDrugtype").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("db unavailable")); + } + + @Test + @DisplayName("createManufacturer should report a parse failure for a malformed body") + void createManufacturer_shouldReportParseFailure() throws Exception { + mockMvc.perform(post("/createDrugtype").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("not-json")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getManufacturer should look the rows up by the posted provider service map id") + void getManufacturer_shouldLookUpByProviderServiceMapId() throws Exception { + when(drugtypeInter.getDrugtypeData(3)).thenReturn(rows(row(1, "first"), row(2, "second"))); + + mockMvc.perform(post("/getDrugtype").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.length()").value(2)); + + verify(drugtypeInter).getDrugtypeData(3); + } + + @Test + @DisplayName("getManufacturer should report the failure when the lookup throws") + void getManufacturer_shouldReportServiceFailure() throws Exception { + when(drugtypeInter.getDrugtypeData(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getDrugtype").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("editManufacturer should copy the editable fields onto the stored row before saving") + void editManufacturer_shouldCopyEditableFieldsBeforeSaving() throws Exception { + M_Drugtype stored = row(1, "old name"); + when(drugtypeInter.editDrugtypeData(1)).thenReturn(stored); + when(drugtypeInter.saveeditDrugtype(any(M_Drugtype.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/editDrugtype").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"drugTypeID\":1,\"drugTypeName\":\"new name\",\"drugTypeDesc\":\"new desc\"," + + "\"drugTypeCode\":\"NEW\",\"status\":\"Inactive\",\"modifiedBy\":\"tester\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.drugTypeName").value("new name")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Drugtype.class); + verify(drugtypeInter).saveeditDrugtype(captor.capture()); + assertEquals("tester", captor.getValue().getModifiedBy()); + } + + @Test + @DisplayName("editManufacturer should report the failure when the row cannot be found") + void editManufacturer_shouldReportFailureWhenRowMissing() throws Exception { + when(drugtypeInter.editDrugtypeData(1)).thenReturn(null); + + mockMvc.perform(post("/editDrugtype").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"drugTypeID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("deleteManufacturer should flip the deleted flag on the stored row and save it") + void deleteManufacturer_shouldFlipDeletedFlag() throws Exception { + M_Drugtype stored = row(1, "first"); + when(drugtypeInter.editDrugtypeData(1)).thenReturn(stored); + when(drugtypeInter.saveeditDrugtype(any(M_Drugtype.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/deleteDrugtype").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"drugTypeID\":1,\"deleted\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Drugtype.class); + verify(drugtypeInter).saveeditDrugtype(captor.capture()); + assertEquals(Boolean.TRUE, captor.getValue().getDeleted()); + } + + @Test + @DisplayName("deleteManufacturer should report the failure when the row cannot be found") + void deleteManufacturer_shouldReportFailureWhenRowMissing() throws Exception { + when(drugtypeInter.editDrugtypeData(1)).thenReturn(null); + + mockMvc.perform(post("/deleteDrugtype").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"drugTypeID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/facilitytype/FacilitytypeControllerTest.java b/src/test/java/com/iemr/inventory/controller/facilitytype/FacilitytypeControllerTest.java new file mode 100644 index 00000000..681fddfd --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/facilitytype/FacilitytypeControllerTest.java @@ -0,0 +1,202 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.facilitytype; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.facilitytype.M_facilitytype; +import com.iemr.inventory.service.facilitytype.M_facilitytypeInter; + +@ExtendWith(MockitoExtension.class) +@DisplayName("FacilitytypeController Test Suite") +class FacilitytypeControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private M_facilitytypeInter m_facilitytypeInter; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked facility type service") + void setUp() { + FacilitytypeController controller = new FacilitytypeController(); + ReflectionTestUtils.setField(controller, "m_facilitytypeInter", m_facilitytypeInter); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static M_facilitytype row(Integer id, String name) { + M_facilitytype row = new M_facilitytype(); + row.setFacilityTypeID(id); + row.setFacilityTypeName(name); + row.setProviderServiceMapID(3); + return row; + } + + private static ArrayList rows(M_facilitytype... items) { + return new ArrayList<>(List.of(items)); + } + + @Test + @DisplayName("addFacility should persist the posted array and answer with the saved rows") + void addFacility_shouldPersistPostedArray() throws Exception { + when(m_facilitytypeInter.addAllFicilityData(anyList())).thenReturn(rows(row(1, "first"))); + + mockMvc.perform(post("/addFacility").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{\"facilityTypeName\":\"first\",\"providerServiceMapID\":3}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].facilityTypeName").value("first")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(m_facilitytypeInter).addAllFicilityData(captor.capture()); + assertEquals(1, captor.getValue().size()); + } + + @Test + @DisplayName("addFacility should report the failure when the service blows up") + void addFacility_shouldReportServiceFailure() throws Exception { + when(m_facilitytypeInter.addAllFicilityData(anyList())).thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/addFacility").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("db unavailable")); + } + + @Test + @DisplayName("addFacility should report a parse failure for a malformed body") + void addFacility_shouldReportParseFailure() throws Exception { + mockMvc.perform(post("/addFacility").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("not-json")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getFacility should look the rows up by the posted provider service map id") + void getFacility_shouldLookUpByProviderServiceMapId() throws Exception { + when(m_facilitytypeInter.getAllFicilityData(3)).thenReturn(rows(row(1, "first"), row(2, "second"))); + + mockMvc.perform(post("/getFacility").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.length()").value(2)); + + verify(m_facilitytypeInter).getAllFicilityData(3); + } + + @Test + @DisplayName("getFacility should report the failure when the lookup throws") + void getFacility_shouldReportServiceFailure() throws Exception { + when(m_facilitytypeInter.getAllFicilityData(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getFacility").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("editFacility should copy the editable fields onto the stored row before saving") + void editFacility_shouldCopyEditableFieldsBeforeSaving() throws Exception { + M_facilitytype stored = row(1, "old name"); + when(m_facilitytypeInter.editAllFicilityData(1)).thenReturn(stored); + when(m_facilitytypeInter.updateFacilityData(any(M_facilitytype.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/editFacility").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityTypeID\":1,\"facilityTypeName\":\"new name\",\"facilityTypeDesc\":\"new desc\"," + + "\"facilityTypeCode\":\"NEW\",\"modifiedBy\":\"tester\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.facilityTypeName").value("new name")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_facilitytype.class); + verify(m_facilitytypeInter).updateFacilityData(captor.capture()); + assertEquals("tester", captor.getValue().getModifiedBy()); + } + + @Test + @DisplayName("editFacility should report the failure when the row cannot be found") + void editFacility_shouldReportFailureWhenRowMissing() throws Exception { + when(m_facilitytypeInter.editAllFicilityData(1)).thenReturn(null); + + mockMvc.perform(post("/editFacility").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityTypeID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("deleteFacility should flip the deleted flag on the stored row and save it") + void deleteFacility_shouldFlipDeletedFlag() throws Exception { + M_facilitytype stored = row(1, "first"); + when(m_facilitytypeInter.editAllFicilityData(1)).thenReturn(stored); + when(m_facilitytypeInter.updateFacilityData(any(M_facilitytype.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/deleteFacility").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityTypeID\":1,\"deleted\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_facilitytype.class); + verify(m_facilitytypeInter).updateFacilityData(captor.capture()); + assertEquals(Boolean.TRUE, captor.getValue().getDeleted()); + } + + @Test + @DisplayName("deleteFacility should report the failure when the row cannot be found") + void deleteFacility_shouldReportFailureWhenRowMissing() throws Exception { + when(m_facilitytypeInter.editAllFicilityData(1)).thenReturn(null); + + mockMvc.perform(post("/deleteFacility").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityTypeID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/health/HealthControllerTest.java b/src/test/java/com/iemr/inventory/controller/health/HealthControllerTest.java new file mode 100644 index 00000000..5cb9a4ac --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/health/HealthControllerTest.java @@ -0,0 +1,106 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.health; + +import java.util.LinkedHashMap; +import java.util.Map; + +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 org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.service.health.HealthService; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +@DisplayName("HealthController Test Suite") +class HealthControllerTest { + + @Mock + private HealthService healthService; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Set up standalone MockMvc before each test") + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(new HealthController(healthService)).build(); + } + + private Map healthResponse(String overallStatus) { + Map response = new LinkedHashMap<>(); + response.put("status", overallStatus); + response.put("checkedAt", "2025-06-25T10:00:00Z"); + return response; + } + + @Test + @DisplayName("checkHealth should return 200 with the payload when all services are UP") + void checkHealth_shouldReturnOkWhenStatusIsUp() throws Exception { + when(healthService.checkHealth()).thenReturn(healthResponse("UP")); + + mockMvc.perform(get("/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.checkedAt").value("2025-06-25T10:00:00Z")); + } + + @Test + @DisplayName("checkHealth should return 200 when DEGRADED, since the instance is still operational") + void checkHealth_shouldReturnOkWhenStatusIsDegraded() throws Exception { + when(healthService.checkHealth()).thenReturn(healthResponse("DEGRADED")); + + mockMvc.perform(get("/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("DEGRADED")); + } + + @Test + @DisplayName("checkHealth should return 503 when a critical service is DOWN") + void checkHealth_shouldReturnServiceUnavailableWhenStatusIsDown() throws Exception { + when(healthService.checkHealth()).thenReturn(healthResponse("DOWN")); + + mockMvc.perform(get("/health")) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status").value("DOWN")); + } + + @Test + @DisplayName("checkHealth should return 503 with a DOWN payload when the service throws unexpectedly") + void checkHealth_shouldReturnServiceUnavailableWhenServiceThrows() throws Exception { + when(healthService.checkHealth()).thenThrow(new IllegalStateException("unexpected failure")); + + mockMvc.perform(get("/health")) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status").value("DOWN")) + .andExpect(jsonPath("$.timestamp").exists()); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/indent/IndentControllerTest.java b/src/test/java/com/iemr/inventory/controller/indent/IndentControllerTest.java new file mode 100644 index 00000000..bd21d1cb --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/indent/IndentControllerTest.java @@ -0,0 +1,323 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.indent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.indent.Indent; +import com.iemr.inventory.data.indent.IndentIssue; +import com.iemr.inventory.data.indent.IndentOrder; +import com.iemr.inventory.data.indent.ItemfacilitymappingIndent; +import com.iemr.inventory.service.indent.IndentService; + +@ExtendWith(MockitoExtension.class) +@DisplayName("IndentController Test Suite") +class IndentControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private IndentService indentService; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked indent service") + void setUp() { + IndentController controller = new IndentController(); + ReflectionTestUtils.setField(controller, "IndentService", indentService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + @DisplayName("partialsearchindentitems should trim the posted item name before searching") + void partialsearchindentitems_shouldTrimItemName() throws Exception { + when(indentService.findItemIndent(1, "Para")).thenReturn(List.of(new ItemfacilitymappingIndent())); + + mockMvc.perform(post("/indentController/partialsearchindentitems").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemName\":\" Para \",\"facilityID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(indentService).findItemIndent(1, "Para"); + } + + @Test + @DisplayName("partialsearchindentitems should report the failure when the search throws") + void partialsearchindentitems_shouldReportServiceFailure() throws Exception { + when(indentService.findItemIndent(anyInt(), anyString())).thenThrow(new RuntimeException("search failed")); + + mockMvc.perform(post("/indentController/partialsearchindentitems").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemName\":\"Para\",\"facilityID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("createIndentRequest should hand the posted indent to the service") + void createIndentRequest_shouldHandIndentToService() throws Exception { + when(indentService.createIndentRequest(any(Indent.class))).thenReturn("{\"indentID\":88}"); + + mockMvc.perform(post("/indentController/createIndentRequest").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"fromFacilityID\":1,\"toFacilityID\":2,\"indentOrder\":[]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.indentID").value(88)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Indent.class); + verify(indentService).createIndentRequest(captor.capture()); + assertEquals(1, captor.getValue().getFromFacilityID()); + } + + @Test + @DisplayName("createIndentRequest should report the failure when the service throws") + void createIndentRequest_shouldReportServiceFailure() throws Exception { + when(indentService.createIndentRequest(any(Indent.class))).thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/indentController/createIndentRequest").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"fromFacilityID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getIndentHistory should hand the posted indent probe to the service") + void getIndentHistory_shouldHandProbeToService() throws Exception { + when(indentService.getIndentHistory(any(Indent.class))).thenReturn("[]"); + + mockMvc.perform(post("/indentController/getIndentHistory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"fromFacilityID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Indent.class); + verify(indentService).getIndentHistory(captor.capture()); + assertEquals(1, captor.getValue().getFromFacilityID()); + } + + @Test + @DisplayName("getIndentHistory should report the failure when the lookup throws") + void getIndentHistory_shouldReportServiceFailure() throws Exception { + when(indentService.getIndentHistory(any(Indent.class))).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/indentController/getIndentHistory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"fromFacilityID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getOrdersByIndentID should hand the posted order probe to the service") + void getOrdersByIndentID_shouldHandProbeToService() throws Exception { + when(indentService.getOrdersByIndentID(any(IndentOrder.class))).thenReturn("[]"); + + mockMvc.perform(post("/indentController/getOrdersByIndentID").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"indentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(IndentOrder.class); + verify(indentService).getOrdersByIndentID(captor.capture()); + assertEquals(88L, captor.getValue().getIndentID()); + } + + @Test + @DisplayName("getOrdersByIndentID should report the failure when the lookup throws") + void getOrdersByIndentID_shouldReportServiceFailure() throws Exception { + when(indentService.getOrdersByIndentID(any(IndentOrder.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/indentController/getOrdersByIndentID").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"indentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getIndentWorklist should hand the posted worklist filter to the service") + void getIndentWorklist_shouldHandFilterToService() throws Exception { + when(indentService.getIndentWorklist(any(IndentOrder.class))).thenReturn("[]"); + + mockMvc.perform(post("/indentController/getIndentWorklist").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":2}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(IndentOrder.class); + verify(indentService).getIndentWorklist(captor.capture()); + assertEquals(2, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getIndentWorklist should report the failure when the lookup throws") + void getIndentWorklist_shouldReportServiceFailure() throws Exception { + when(indentService.getIndentWorklist(any(IndentOrder.class))).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/indentController/getIndentWorklist").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":2}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getIndentOrderWorklist should hand the posted order probe to the service") + void getIndentOrderWorklist_shouldHandProbeToService() throws Exception { + when(indentService.getIndentOrderWorklist(any(IndentOrder.class))).thenReturn("[]"); + + mockMvc.perform(post("/indentController/getIndentOrderWorklist").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"indentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(indentService).getIndentOrderWorklist(any(IndentOrder.class)); + } + + @Test + @DisplayName("getIndentOrderWorklist should report the failure when the lookup throws") + void getIndentOrderWorklist_shouldReportServiceFailure() throws Exception { + when(indentService.getIndentOrderWorklist(any(IndentOrder.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/indentController/getIndentOrderWorklist").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"indentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("issueIndent should hand the posted array of issue lines to the service") + void issueIndent_shouldHandIssueLinesToService() throws Exception { + when(indentService.issueIndent(any(IndentIssue[].class))).thenReturn("Dispensed successfully"); + + mockMvc.perform(post("/indentController/issueIndent").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("[{\"indentID\":88,\"action\":\"Issued\",\"issuedQty\":6}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.response").value("Dispensed successfully")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(IndentIssue[].class); + verify(indentService).issueIndent(captor.capture()); + assertEquals("Issued", captor.getValue()[0].getAction()); + } + + @Test + @DisplayName("issueIndent should report the failure when the issue throws") + void issueIndent_shouldReportServiceFailure() throws Exception { + when(indentService.issueIndent(any(IndentIssue[].class))).thenThrow(new RuntimeException("issue failed")); + + mockMvc.perform(post("/indentController/issueIndent").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("cancelIndentOrder should hand the posted indent to the service") + void cancelIndentOrder_shouldHandIndentToService() throws Exception { + when(indentService.cancelIndentOrder(any(Indent.class))).thenReturn("Cancelled successfully"); + + mockMvc.perform(post("/indentController/cancelIndentOrder").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"indentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.response").value("Cancelled successfully")); + } + + @Test + @DisplayName("cancelIndentOrder should report the failure when the cancellation throws") + void cancelIndentOrder_shouldReportServiceFailure() throws Exception { + when(indentService.cancelIndentOrder(any(Indent.class))).thenThrow(new RuntimeException("cancel failed")); + + mockMvc.perform(post("/indentController/cancelIndentOrder").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"indentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("receiveIndent should hand the posted indent to the service") + void receiveIndent_shouldHandIndentToService() throws Exception { + when(indentService.receiveIndent(any(Indent.class))).thenReturn("Received successfully"); + + mockMvc.perform(post("/indentController/receiveIndent").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"indentID\":88,\"fromFacilityID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.response").value("Received successfully")); + } + + @Test + @DisplayName("receiveIndent should report the failure when the receipt throws") + void receiveIndent_shouldReportServiceFailure() throws Exception { + when(indentService.receiveIndent(any(Indent.class))).thenThrow(new RuntimeException("receive failed")); + + mockMvc.perform(post("/indentController/receiveIndent").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"indentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("updateIndentOrder should hand the posted indent to the service") + void updateIndentOrder_shouldHandIndentToService() throws Exception { + when(indentService.updateIndentOrder(any(Indent.class))).thenReturn("Updated successfully"); + + mockMvc.perform(post("/indentController/updateIndentOrder").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"indentID\":88,\"fromFacilityID\":1,\"indentOrder\":[]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.response").value("Updated successfully")); + } + + @Test + @DisplayName("updateIndentOrder should report the failure when the update throws") + void updateIndentOrder_shouldReportServiceFailure() throws Exception { + when(indentService.updateIndentOrder(any(Indent.class))).thenThrow(new RuntimeException("update failed")); + + mockMvc.perform(post("/indentController/updateIndentOrder").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"indentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/item/ItemControllerTest.java b/src/test/java/com/iemr/inventory/controller/item/ItemControllerTest.java new file mode 100644 index 00000000..8d7699fa --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/item/ItemControllerTest.java @@ -0,0 +1,355 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.item; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.items.ItemMaster; +import com.iemr.inventory.data.items.M_ItemCategory; +import com.iemr.inventory.data.items.M_ItemForm; +import com.iemr.inventory.data.items.M_Route; +import com.iemr.inventory.service.item.ItemService; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ItemController Test Suite") +class ItemControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private ItemService itemService; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked item service") + void setUp() { + ItemController controller = new ItemController(); + ReflectionTestUtils.setField(controller, "itemService", itemService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static ItemMaster item(Integer id, String name) { + ItemMaster item = new ItemMaster(); + item.setItemID(id); + item.setItemName(name); + item.setProviderServiceMapID(3); + return item; + } + + @Test + @DisplayName("getItemForm should return the forms configured for the provider service map") + void getItemForm_shouldReturnFormsForProviderServiceMap() throws Exception { + when(itemService.getItemFormProviderServiceMapID(3)).thenReturn(List.of(new M_ItemForm())); + + mockMvc.perform(get("/getItemForm/3").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.length()").value(1)); + } + + @Test + @DisplayName("getItemForm should report the failure when the lookup throws") + void getItemForm_shouldReportServiceFailure() throws Exception { + when(itemService.getItemFormProviderServiceMapID(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(get("/getItemForm/3").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getItemRoute should return the routes configured for the provider service map") + void getItemRoute_shouldReturnRoutesForProviderServiceMap() throws Exception { + when(itemService.getItemRouteProviderServiceMapID(3)).thenReturn(List.of(new M_Route())); + + mockMvc.perform(get("/getItemRoute/3").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getItemRoute should report the failure when the lookup throws") + void getItemRoute_shouldReportServiceFailure() throws Exception { + when(itemService.getItemRouteProviderServiceMapID(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(get("/getItemRoute/3").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getItemCategory should ask for every category when the flag path segment is zero") + void getItemCategory_shouldAskForEveryCategoryWhenFlagIsZero() throws Exception { + when(itemService.getItemCategory(true, 3)).thenReturn(List.of(new M_ItemCategory())); + + mockMvc.perform(get("/getItemCategory/3/0").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(itemService).getItemCategory(true, 3); + } + + @Test + @DisplayName("getItemCategory should ask for the live categories only when the flag path segment is not zero") + void getItemCategory_shouldAskForLiveCategoriesWhenFlagIsNotZero() throws Exception { + when(itemService.getItemCategory(false, 3)).thenReturn(List.of(new M_ItemCategory())); + + mockMvc.perform(get("/getItemCategory/3/1").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(itemService).getItemCategory(false, 3); + } + + @Test + @DisplayName("getItemCategory should report the failure when the lookup throws") + void getItemCategory_shouldReportServiceFailure() throws Exception { + when(itemService.getItemCategory(true, 3)).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(get("/getItemCategory/3/0").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("createItemMaster should save the posted batch and answer with the stored rows") + void createItemMaster_shouldSavePostedBatch() throws Exception { + when(itemService.addAllItemMaster(anyList())).thenReturn(List.of(item(1, "Paracetamol"))); + + mockMvc.perform(post("/createItemMaster").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("[{\"itemName\":\"Paracetamol\",\"providerServiceMapID\":3}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].itemName").value("Paracetamol")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(itemService).addAllItemMaster(captor.capture()); + assertEquals(1, captor.getValue().size()); + } + + @Test + @DisplayName("createItemMaster should report the failure when the save throws") + void createItemMaster_shouldReportServiceFailure() throws Exception { + when(itemService.addAllItemMaster(anyList())).thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/createItemMaster").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getItemMaster should return the items of the provider service map") + void getItemMaster_shouldReturnItemsOfProviderServiceMap() throws Exception { + when(itemService.getItemMaster(3)).thenReturn(List.of(item(1, "Paracetamol"))); + + mockMvc.perform(get("/getItemMaster/3").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].itemName").value("Paracetamol")); + } + + @Test + @DisplayName("getItemMaster should report the failure when the lookup throws") + void getItemMaster_shouldReportServiceFailure() throws Exception { + when(itemService.getItemMaster(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(get("/getItemMaster/3").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getActiveItemMaster should pass the posted probe item to the service") + void getActiveItemMaster_shouldPassProbeItemThrough() throws Exception { + when(itemService.getActiveItemMaster(any(ItemMaster.class))).thenReturn(List.of(item(1, "Paracetamol"))); + + mockMvc.perform(post("/getActiveItemMaster").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"deleted\":false,\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemMaster.class); + verify(itemService).getActiveItemMaster(captor.capture()); + assertEquals(Boolean.FALSE, captor.getValue().getDeleted()); + assertEquals(3, captor.getValue().getProviderServiceMapID()); + } + + @Test + @DisplayName("getActiveItemMaster should report the failure when the lookup throws") + void getActiveItemMaster_shouldReportServiceFailure() throws Exception { + when(itemService.getActiveItemMaster(any(ItemMaster.class))).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getActiveItemMaster").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("blockItemMaster should pass the item id and the delete flag straight through") + void blockItemMaster_shouldPassIdAndFlagThrough() throws Exception { + when(itemService.blockItemMaster(9, true)).thenReturn(1); + + mockMvc.perform(get("/blockItemMaster/9/true").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.response").value("1")); + + verify(itemService).blockItemMaster(9, true); + } + + @Test + @DisplayName("blockItemMaster should report the failure when the update throws") + void blockItemMaster_shouldReportServiceFailure() throws Exception { + when(itemService.blockItemMaster(9, true)).thenThrow(new RuntimeException("update failed")); + + mockMvc.perform(get("/blockItemMaster/9/true").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("discontinueItemMaster should pass the item id and the discontinue flag straight through") + void discontinueItemMaster_shouldPassIdAndFlagThrough() throws Exception { + when(itemService.discontinueItemMaster(9, false)).thenReturn(1); + + mockMvc.perform(get("/discontinueItemMaster/9/false").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(itemService).discontinueItemMaster(9, false); + } + + @Test + @DisplayName("discontinueItemMaster should report the failure when the update throws") + void discontinueItemMaster_shouldReportServiceFailure() throws Exception { + when(itemService.discontinueItemMaster(9, false)).thenThrow(new RuntimeException("update failed")); + + mockMvc.perform(get("/discontinueItemMaster/9/false").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("editItemMaster should copy the description and modifier onto the stored item before saving") + void editItemMaster_shouldCopyEditableFields() throws Exception { + ItemMaster stored = item(9, "Paracetamol"); + when(itemService.getItemMasterByID(9)).thenReturn(stored); + when(itemService.createItemMaster(any(ItemMaster.class))).thenAnswer(inv -> inv.getArgument(0)); + + mockMvc.perform(post("/editItemMaster").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"itemID\":9,\"itemDesc\":\"new desc\",\"modifiedBy\":\"tester\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemMaster.class); + verify(itemService).createItemMaster(captor.capture()); + assertEquals("new desc", captor.getValue().getItemDesc()); + assertEquals("tester", captor.getValue().getModifiedBy()); + } + + @Test + @DisplayName("editItemMaster should report the failure when the item cannot be found") + void editItemMaster_shouldReportFailureWhenItemMissing() throws Exception { + when(itemService.getItemMasterByID(9)).thenReturn(null); + + mockMvc.perform(post("/editItemMaster").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemID\":9}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("configItemIssue should push the posted categories into the issue configuration") + void configItemIssue_shouldUpdateIssueConfiguration() throws Exception { + when(itemService.updateItemIssueConfig(anyList())).thenReturn(2); + + mockMvc.perform(post("/configItemIssue").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("[{\"itemCategoryID\":1,\"issueType\":\"Bulk\"},{\"itemCategoryID\":2,\"issueType\":\"Single\"}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.response").value("2")); + } + + @Test + @DisplayName("configItemIssue should report the failure when the update throws") + void configItemIssue_shouldReportServiceFailure() throws Exception { + when(itemService.updateItemIssueConfig(anyList())).thenThrow(new RuntimeException("update failed")); + + mockMvc.perform(post("/configItemIssue").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getItem should narrow the items to the posted provider service map and category") + void getItem_shouldNarrowByProviderServiceMapAndCategory() throws Exception { + when(itemService.getItemMasters(3, 5)).thenReturn(List.of(item(1, "Paracetamol"))); + + mockMvc.perform(post("/getItem").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3,\"itemCategoryID\":5}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(itemService).getItemMasters(3, 5); + } + + @Test + @DisplayName("getItem should report the failure when the lookup throws") + void getItem_shouldReportServiceFailure() throws Exception { + when(itemService.getItemMasters(anyInt(), anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getItem").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3,\"itemCategoryID\":5}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/itemfacilitymapping/ItemfacilitymappingControllerTest.java b/src/test/java/com/iemr/inventory/controller/itemfacilitymapping/ItemfacilitymappingControllerTest.java new file mode 100644 index 00000000..ef244522 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/itemfacilitymapping/ItemfacilitymappingControllerTest.java @@ -0,0 +1,314 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.itemfacilitymapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.items.ItemInStore; +import com.iemr.inventory.data.items.ItemMaster; +import com.iemr.inventory.data.itemfacilitymapping.M_itemfacilitymapping; +import com.iemr.inventory.data.itemfacilitymapping.V_fetchItemFacilityMap; +import com.iemr.inventory.data.stockentry.ItemStockEntry; +import com.iemr.inventory.service.itemfacilitymapping.M_itemfacilitymappingInter; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ItemfacilitymappingController Test Suite") +class ItemfacilitymappingControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private M_itemfacilitymappingInter m_itemfacilitymappingInter; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked item-facility mapping service") + void setUp() { + ItemfacilitymappingController controller = new ItemfacilitymappingController(); + ReflectionTestUtils.setField(controller, "M_itemfacilitymappingInter", m_itemfacilitymappingInter); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static M_itemfacilitymapping mapping(Integer id) { + M_itemfacilitymapping mapping = new M_itemfacilitymapping(); + mapping.setItemStoreMapID(id); + mapping.setFacilityID(7); + mapping.setItemID(11); + return mapping; + } + + @Test + @DisplayName("mapItemtoStrore should fan the posted item id array out into one mapping row per item") + void mapItemtoStrore_shouldFanItemArrayIntoRows() throws Exception { + when(m_itemfacilitymappingInter.mapItemtoStore(anyList())) + .thenReturn(new ArrayList<>(List.of(mapping(1), mapping(2)))); + + mockMvc.perform(post("/mapItemtoStrore").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("[{\"facilityID\":7,\"mappingType\":\"Main\",\"providerServiceMapID\":3," + + "\"status\":\"Active\",\"createdBy\":\"tester\",\"itemID1\":[11,12,13]}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(m_itemfacilitymappingInter).mapItemtoStore(captor.capture()); + List sent = captor.getValue(); + assertEquals(3, sent.size()); + assertEquals(11, sent.get(0).getItemID()); + assertEquals(13, sent.get(2).getItemID()); + assertEquals(7, sent.get(0).getFacilityID()); + assertEquals("Main", sent.get(0).getMappingType()); + assertEquals("tester", sent.get(0).getCreatedBy()); + } + + @Test + @DisplayName("mapItemtoStrore should report the failure when the posted payload carries no item id array") + void mapItemtoStrore_shouldReportFailureWhenItemArrayMissing() throws Exception { + mockMvc.perform(post("/mapItemtoStrore").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("[{\"facilityID\":7}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("editItemtoStrore should copy the editable fields onto the stored mapping before saving") + void editItemtoStrore_shouldCopyEditableFields() throws Exception { + M_itemfacilitymapping stored = mapping(5); + when(m_itemfacilitymappingInter.editdata(5)).thenReturn(stored); + when(m_itemfacilitymappingInter.saveEditedItem(any(M_itemfacilitymapping.class))) + .thenAnswer(inv -> inv.getArgument(0)); + + mockMvc.perform(post("/editItemtoStrore").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"itemFacilityMapID\":5,\"facilityID\":9,\"itemID\":22,\"mappingType\":\"Sub\"," + + "\"providerServiceMapID\":3,\"status\":\"Inactive\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_itemfacilitymapping.class); + verify(m_itemfacilitymappingInter).saveEditedItem(captor.capture()); + assertEquals(9, captor.getValue().getFacilityID()); + assertEquals(22, captor.getValue().getItemID()); + assertEquals("Sub", captor.getValue().getMappingType()); + assertEquals("Inactive", captor.getValue().getStatus()); + } + + @Test + @DisplayName("editItemtoStrore should report the failure when the mapping cannot be found") + void editItemtoStrore_shouldReportFailureWhenMappingMissing() throws Exception { + when(m_itemfacilitymappingInter.editdata(5)).thenReturn(null); + + mockMvc.perform(post("/editItemtoStrore").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemFacilityMapID\":5}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("deleteItemtoStrore should flip the deleted flag on the stored mapping and save it") + void deleteItemtoStrore_shouldFlipDeletedFlag() throws Exception { + M_itemfacilitymapping stored = mapping(5); + when(m_itemfacilitymappingInter.editdata(5)).thenReturn(stored); + when(m_itemfacilitymappingInter.saveEditedItem(any(M_itemfacilitymapping.class))) + .thenAnswer(inv -> inv.getArgument(0)); + + mockMvc.perform(post("/deleteItemtoStrore").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"itemFacilityMapID\":5,\"deleted\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_itemfacilitymapping.class); + verify(m_itemfacilitymappingInter).saveEditedItem(captor.capture()); + assertEquals(Boolean.TRUE, captor.getValue().getDeleted()); + } + + @Test + @DisplayName("deleteItemtoStrore should report the failure when the mapping cannot be found") + void deleteItemtoStrore_shouldReportFailureWhenMappingMissing() throws Exception { + when(m_itemfacilitymappingInter.editdata(5)).thenReturn(null); + + mockMvc.perform(post("/deleteItemtoStrore").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemFacilityMapID\":5}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("getSubStroreitem should look the sub-store items up by provider service map and facility") + void getSubStroreitem_shouldLookUpByProviderServiceMapAndFacility() throws Exception { + when(m_itemfacilitymappingInter.getsubitemforsubStote(3, 7)) + .thenReturn(new ArrayList<>(List.of(mapping(1)))); + + mockMvc.perform(post("/getSubStoreitem").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3,\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(m_itemfacilitymappingInter).getsubitemforsubStote(3, 7); + } + + @Test + @DisplayName("getSubStroreitem should report the failure when the lookup throws") + void getSubStroreitem_shouldReportServiceFailure() throws Exception { + when(m_itemfacilitymappingInter.getsubitemforsubStote(anyInt(), anyInt())) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getSubStoreitem").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3,\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getAllFacilityMappedData should look the mapped rows up by provider service map") + void getAllFacilityMappedData_shouldLookUpByProviderServiceMap() throws Exception { + when(m_itemfacilitymappingInter.getAllFacilityMappedData(3)) + .thenReturn(new ArrayList<>(List.of(new V_fetchItemFacilityMap()))); + + mockMvc.perform(post("/getAllFacilityMappedData").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(m_itemfacilitymappingInter).getAllFacilityMappedData(3); + } + + @Test + @DisplayName("getAllFacilityMappedData should report the failure when the lookup throws") + void getAllFacilityMappedData_shouldReportServiceFailure() throws Exception { + when(m_itemfacilitymappingInter.getAllFacilityMappedData(anyInt())) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getAllFacilityMappedData").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getItemFromStoreID should return the items the store holds") + void getItemFromStoreID_shouldReturnItemsOfStore() throws Exception { + when(m_itemfacilitymappingInter.getItemMastersFromStoreID(7)) + .thenReturn(List.of(new ItemInStore(7, 11, "Paracetamol", 40L))); + + mockMvc.perform(post("/getItemFromStoreID/7").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(m_itemfacilitymappingInter).getItemMastersFromStoreID(7); + } + + @Test + @DisplayName("getItemFromStoreID should report the failure when the lookup throws") + void getItemFromStoreID_shouldReportServiceFailure() throws Exception { + when(m_itemfacilitymappingInter.getItemMastersFromStoreID(anyInt())) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getItemFromStoreID/7").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("itemPartialSearch should pass the item name and facility from the posted probe item") + void itemPartialSearch_shouldPassNameAndFacility() throws Exception { + when(m_itemfacilitymappingInter.getItemMastersPartialSearch("Para", 7)) + .thenReturn(List.of(new ItemMaster())); + + mockMvc.perform(post("/itemPartialSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"itemName\":\"Para\",\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(m_itemfacilitymappingInter).getItemMastersPartialSearch("Para", 7); + } + + @Test + @DisplayName("itemPartialSearch should report the failure when the search throws") + void itemPartialSearch_shouldReportServiceFailure() throws Exception { + when(m_itemfacilitymappingInter.getItemMastersPartialSearch(anyString(), anyInt())) + .thenThrow(new RuntimeException("search failed")); + + mockMvc.perform(post("/itemPartialSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"itemName\":\"Para\",\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getItemBatchForStoreTransfer should pass the two facilities and the item name straight through") + void getItemBatchForStoreTransfer_shouldPassTransferDetails() throws Exception { + when(m_itemfacilitymappingInter.getItemBatchForStoreTransfer(1, 2, "Para")) + .thenReturn(List.of(new ItemStockEntry())); + + mockMvc.perform(post("/getItemBatchForStoreTransfer").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"transferFromFacilityID\":1,\"transferToFacilityID\":2,\"itemName\":\"Para\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(m_itemfacilitymappingInter).getItemBatchForStoreTransfer(1, 2, "Para"); + } + + @Test + @DisplayName("getItemBatchForStoreTransfer should report the failure when the lookup throws") + void getItemBatchForStoreTransfer_shouldReportServiceFailure() throws Exception { + when(m_itemfacilitymappingInter.getItemBatchForStoreTransfer(anyInt(), anyInt(), anyString())) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getItemBatchForStoreTransfer").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"transferFromFacilityID\":1,\"transferToFacilityID\":2,\"itemName\":\"Para\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/manufacturer/ManufacturerControllerTest.java b/src/test/java/com/iemr/inventory/controller/manufacturer/ManufacturerControllerTest.java new file mode 100644 index 00000000..23d86143 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/manufacturer/ManufacturerControllerTest.java @@ -0,0 +1,203 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.manufacturer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.manufacturer.M_Manufacturer; +import com.iemr.inventory.service.manufacturer.ManufacturerInter; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ManufacturerController Test Suite") +class ManufacturerControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private ManufacturerInter manufacturerInter; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked manufacturer service") + void setUp() { + ManufacturerController controller = new ManufacturerController(); + ReflectionTestUtils.setField(controller, "manufacturerInter", manufacturerInter); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static M_Manufacturer row(Integer id, String name) { + M_Manufacturer row = new M_Manufacturer(); + row.setManufacturerID(id); + row.setManufacturerName(name); + row.setProviderServiceMapID(3); + return row; + } + + private static ArrayList rows(M_Manufacturer... items) { + return new ArrayList<>(List.of(items)); + } + + @Test + @DisplayName("createManufacturer should persist the posted array and answer with the saved rows") + void createManufacturer_shouldPersistPostedArray() throws Exception { + when(manufacturerInter.createManufacturer(anyList())).thenReturn(rows(row(1, "first"))); + + mockMvc.perform(post("/createManufacturer").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{\"manufacturerName\":\"first\",\"providerServiceMapID\":3}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].manufacturerName").value("first")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(manufacturerInter).createManufacturer(captor.capture()); + assertEquals(1, captor.getValue().size()); + } + + @Test + @DisplayName("createManufacturer should report the failure when the service blows up") + void createManufacturer_shouldReportServiceFailure() throws Exception { + when(manufacturerInter.createManufacturer(anyList())).thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/createManufacturer").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("db unavailable")); + } + + @Test + @DisplayName("createManufacturer should report a parse failure for a malformed body") + void createManufacturer_shouldReportParseFailure() throws Exception { + mockMvc.perform(post("/createManufacturer").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("not-json")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getManufacturer should look the rows up by the posted provider service map id") + void getManufacturer_shouldLookUpByProviderServiceMapId() throws Exception { + when(manufacturerInter.createManufacturer(3)).thenReturn(rows(row(1, "first"), row(2, "second"))); + + mockMvc.perform(post("/getManufacturer").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.length()").value(2)); + + verify(manufacturerInter).createManufacturer(3); + } + + @Test + @DisplayName("getManufacturer should report the failure when the lookup throws") + void getManufacturer_shouldReportServiceFailure() throws Exception { + when(manufacturerInter.createManufacturer(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getManufacturer").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("editManufacturer should copy the editable fields onto the stored row before saving") + void editManufacturer_shouldCopyEditableFieldsBeforeSaving() throws Exception { + M_Manufacturer stored = row(1, "old name"); + when(manufacturerInter.editManufacturer(1)).thenReturn(stored); + when(manufacturerInter.saveEditedData(any(M_Manufacturer.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/editManufacturer").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"manufacturerID\":1,\"manufacturerName\":\"new name\",\"manufacturerDesc\":\"new desc\"," + + "\"manufacturerCode\":\"NEW\",\"status\":\"Inactive\",\"contactPerson\":\"Alex\"," + + "\"cST_GST_No\":\"GST-9\",\"modifiedBy\":\"tester\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.manufacturerName").value("new name")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Manufacturer.class); + verify(manufacturerInter).saveEditedData(captor.capture()); + assertEquals("tester", captor.getValue().getModifiedBy()); + } + + @Test + @DisplayName("editManufacturer should report the failure when the row cannot be found") + void editManufacturer_shouldReportFailureWhenRowMissing() throws Exception { + when(manufacturerInter.editManufacturer(1)).thenReturn(null); + + mockMvc.perform(post("/editManufacturer").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"manufacturerID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("deleteManufacturer should flip the deleted flag on the stored row and save it") + void deleteManufacturer_shouldFlipDeletedFlag() throws Exception { + M_Manufacturer stored = row(1, "first"); + when(manufacturerInter.editManufacturer(1)).thenReturn(stored); + when(manufacturerInter.saveEditedData(any(M_Manufacturer.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/deleteManufacturer").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"manufacturerID\":1,\"deleted\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Manufacturer.class); + verify(manufacturerInter).saveEditedData(captor.capture()); + assertEquals(Boolean.TRUE, captor.getValue().getDeleted()); + } + + @Test + @DisplayName("deleteManufacturer should report the failure when the row cannot be found") + void deleteManufacturer_shouldReportFailureWhenRowMissing() throws Exception { + when(manufacturerInter.editManufacturer(1)).thenReturn(null); + + mockMvc.perform(post("/deleteManufacturer").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"manufacturerID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/patientreturn/PatientReturnControllerTest.java b/src/test/java/com/iemr/inventory/controller/patientreturn/PatientReturnControllerTest.java new file mode 100644 index 00000000..952cf105 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/patientreturn/PatientReturnControllerTest.java @@ -0,0 +1,195 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.patientreturn; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.sql.Timestamp; +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.patientreturn.ItemDetailModel; +import com.iemr.inventory.data.patientreturn.PatientReturnModel; +import com.iemr.inventory.data.patientreturn.ReturnHistoryModel; +import com.iemr.inventory.data.stockExit.ItemReturnEntry; +import com.iemr.inventory.data.stockExit.T_PatientIssue; +import com.iemr.inventory.service.patientreturn.PatientReturnService; + +@ExtendWith(MockitoExtension.class) +@DisplayName("PatientReturnController Test Suite") +class PatientReturnControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private PatientReturnService patientReturnService; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked patient return service") + void setUp() { + PatientReturnController controller = new PatientReturnController(); + ReflectionTestUtils.setField(controller, "patientReturnService", patientReturnService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + @DisplayName("getItemNameByRegID should forward the beneficiary and facility from the posted payload") + void getItemNameByRegID_shouldForwardBeneficiaryAndFacility() throws Exception { + when(patientReturnService.getItemNameByRegID(any(T_PatientIssue.class))) + .thenReturn(List.of(new PatientReturnModel(101L, 7, 11, "Paracetamol"))); + + mockMvc.perform(post("/patientReturnController/getItemNameByRegID").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"benRegID\":101,\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].itemName").value("Paracetamol")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(T_PatientIssue.class); + verify(patientReturnService).getItemNameByRegID(captor.capture()); + assertEquals(101L, captor.getValue().getBenRegID()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getItemNameByRegID should report the failure when the lookup throws") + void getItemNameByRegID_shouldReportServiceFailure() throws Exception { + when(patientReturnService.getItemNameByRegID(any(T_PatientIssue.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/patientReturnController/getItemNameByRegID").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"benRegID\":101,\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getItemDetailByBen should forward the beneficiary, item and facility from the posted payload") + void getItemDetailByBen_shouldForwardProbe() throws Exception { + when(patientReturnService.getItemDetailByBen(any(ItemDetailModel.class))) + .thenReturn(List.of(new ItemDetailModel(11, "Paracetamol", "B-1", 20, + Timestamp.valueOf("2025-01-31 10:15:30"), false, false, 501L, 601L, 701L, 801L, 101L, 3, 7))); + + mockMvc.perform(post("/patientReturnController/getItemDetailByBen").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"benRegID\":101,\"itemID\":11,\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].batchNo").value("B-1")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemDetailModel.class); + verify(patientReturnService).getItemDetailByBen(captor.capture()); + assertEquals(11, captor.getValue().getItemID()); + } + + @Test + @DisplayName("getItemDetailByBen should report the failure when the lookup throws") + void getItemDetailByBen_shouldReportServiceFailure() throws Exception { + when(patientReturnService.getItemDetailByBen(any(ItemDetailModel.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/patientReturnController/getItemDetailByBen").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"benRegID\":101}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("updateQuantityReturned should pass the posted array of returned lines to the service") + void updateQuantityReturned_shouldPassReturnedLines() throws Exception { + when(patientReturnService.updateQuantityReturned(any(ItemDetailModel[].class))) + .thenReturn("Quantity updated successfully"); + + mockMvc.perform(post("/patientReturnController/updateQuantityReturned").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("[{\"itemStockEntryID\":601,\"itemStockExitID\":501,\"returnQuantity\":4}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.response").value("Quantity updated successfully")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemDetailModel[].class); + verify(patientReturnService).updateQuantityReturned(captor.capture()); + assertEquals(1, captor.getValue().length); + assertEquals(4, captor.getValue()[0].getReturnQuantity()); + } + + @Test + @DisplayName("updateQuantityReturned should report the failure when the update throws") + void updateQuantityReturned_shouldReportServiceFailure() throws Exception { + when(patientReturnService.updateQuantityReturned(any(ItemDetailModel[].class))) + .thenThrow(new RuntimeException("update failed")); + + mockMvc.perform(post("/patientReturnController/updateQuantityReturned").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getBenReturnHistory should forward the facility and the date window from the posted payload") + void getBenReturnHistory_shouldForwardWindow() throws Exception { + when(patientReturnService.getBenReturnHistory(any(ItemReturnEntry.class))) + .thenReturn(List.of(new ReturnHistoryModel("Paracetamol", "B-1", 20, + Timestamp.valueOf("2025-01-31 10:15:30"), 701L, 801L, "Jane Doe", 34, "Female", + Timestamp.valueOf("2025-02-01 09:00:00")))); + + mockMvc.perform(post("/patientReturnController/getBenReturnHistory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].patientName").value("Jane Doe")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemReturnEntry.class); + verify(patientReturnService).getBenReturnHistory(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getBenReturnHistory should report the failure when the lookup throws") + void getBenReturnHistory_shouldReportServiceFailure() throws Exception { + when(patientReturnService.getBenReturnHistory(any(ItemReturnEntry.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/patientReturnController/getBenReturnHistory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/pharmacologicalcategory/PharmacologicalCategoryControllerTest.java b/src/test/java/com/iemr/inventory/controller/pharmacologicalcategory/PharmacologicalCategoryControllerTest.java new file mode 100644 index 00000000..63297885 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/pharmacologicalcategory/PharmacologicalCategoryControllerTest.java @@ -0,0 +1,202 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.pharmacologicalcategory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.pharmacologicalcategory.M_Pharmacologicalcategory; +import com.iemr.inventory.service.pharmacologicalcategory.PharmacologicalcategoryInter; + +@ExtendWith(MockitoExtension.class) +@DisplayName("PharmacologicalCategoryController Test Suite") +class PharmacologicalCategoryControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private PharmacologicalcategoryInter pharmacologicalcategoryInter; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked pharmacological category service") + void setUp() { + PharmacologicalCategoryController controller = new PharmacologicalCategoryController(); + ReflectionTestUtils.setField(controller, "pharmacologicalcategoryInter", pharmacologicalcategoryInter); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static M_Pharmacologicalcategory row(Integer id, String name) { + M_Pharmacologicalcategory row = new M_Pharmacologicalcategory(); + row.setPharmCategoryID(id); + row.setPharmCategoryName(name); + row.setProviderServiceMapID(3); + return row; + } + + private static ArrayList rows(M_Pharmacologicalcategory... items) { + return new ArrayList<>(List.of(items)); + } + + @Test + @DisplayName("createPharmacologicalcategory should persist the posted array and answer with the saved rows") + void createPharmacologicalcategory_shouldPersistPostedArray() throws Exception { + when(pharmacologicalcategoryInter.createPharmacologicalcategory(anyList())).thenReturn(rows(row(1, "Analgesic"))); + + mockMvc.perform(post("/createPharmacologicalcategory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("[{\"pharmCategoryName\":\"Analgesic\",\"providerServiceMapID\":3}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].pharmCategoryName").value("Analgesic")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(pharmacologicalcategoryInter).createPharmacologicalcategory(captor.capture()); + assertEquals(1, captor.getValue().size()); + } + + @Test + @DisplayName("createPharmacologicalcategory should report the failure when the service blows up") + void createPharmacologicalcategory_shouldReportServiceFailure() throws Exception { + when(pharmacologicalcategoryInter.createPharmacologicalcategory(anyList())) + .thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/createPharmacologicalcategory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("db unavailable")); + } + + @Test + @DisplayName("getPharmacologicalcategory should look the rows up by the posted provider service map id") + void getPharmacologicalcategory_shouldLookUpByProviderServiceMapId() throws Exception { + when(pharmacologicalcategoryInter.getPharmacologicalcategory(3)) + .thenReturn(rows(row(1, "Analgesic"), row(2, "Antibiotic"))); + + mockMvc.perform(post("/getPharmacologicalcategory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.length()").value(2)); + + verify(pharmacologicalcategoryInter).getPharmacologicalcategory(3); + } + + @Test + @DisplayName("getPharmacologicalcategory should report the failure when the lookup throws") + void getPharmacologicalcategory_shouldReportServiceFailure() throws Exception { + when(pharmacologicalcategoryInter.getPharmacologicalcategory(anyInt())) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getPharmacologicalcategory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("editPharmacologicalcategory should copy the description and modifier onto the stored row") + void editPharmacologicalcategory_shouldCopyEditableFields() throws Exception { + M_Pharmacologicalcategory stored = row(1, "Analgesic"); + when(pharmacologicalcategoryInter.editPharmacologicalcategory(1)).thenReturn(stored); + when(pharmacologicalcategoryInter.saveEditedPharData(any(M_Pharmacologicalcategory.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/editPharmacologicalcategory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"pharmacologyCategoryID\":1,\"pharmCategoryDesc\":\"new desc\",\"modifiedBy\":\"tester\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.pharmCategoryDesc").value("new desc")); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(M_Pharmacologicalcategory.class); + verify(pharmacologicalcategoryInter).saveEditedPharData(captor.capture()); + assertEquals("tester", captor.getValue().getModifiedBy()); + } + + @Test + @DisplayName("editPharmacologicalcategory should report the failure when the row cannot be found") + void editPharmacologicalcategory_shouldReportFailureWhenRowMissing() throws Exception { + when(pharmacologicalcategoryInter.editPharmacologicalcategory(1)).thenReturn(null); + + mockMvc.perform(post("/editPharmacologicalcategory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"pharmacologyCategoryID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("deletePharmacologicalcategory should flip the deleted flag on the stored row and save it") + void deletePharmacologicalcategory_shouldFlipDeletedFlag() throws Exception { + M_Pharmacologicalcategory stored = row(1, "Analgesic"); + when(pharmacologicalcategoryInter.editPharmacologicalcategory(1)).thenReturn(stored); + when(pharmacologicalcategoryInter.saveEditedPharData(any(M_Pharmacologicalcategory.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/deletePharmacologicalcategory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"pharmacologyCategoryID\":1,\"deleted\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(M_Pharmacologicalcategory.class); + verify(pharmacologicalcategoryInter).saveEditedPharData(captor.capture()); + assertEquals(Boolean.TRUE, captor.getValue().getDeleted()); + } + + @Test + @DisplayName("deletePharmacologicalcategory should report the failure when the row cannot be found") + void deletePharmacologicalcategory_shouldReportFailureWhenRowMissing() throws Exception { + when(pharmacologicalcategoryInter.editPharmacologicalcategory(1)).thenReturn(null); + + mockMvc.perform(post("/deletePharmacologicalcategory").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"pharmacologyCategoryID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/report/CRMReportControllerTest.java b/src/test/java/com/iemr/inventory/controller/report/CRMReportControllerTest.java new file mode 100644 index 00000000..1421dc70 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/report/CRMReportControllerTest.java @@ -0,0 +1,349 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.report; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.report.ItemStockEntryReport; +import com.iemr.inventory.data.report.ItemStockExitReport; +import com.iemr.inventory.data.report.PatientIssueExitReport; +import com.iemr.inventory.service.report.CRMReportService; + +@ExtendWith(MockitoExtension.class) +@DisplayName("CRMReportController Test Suite") +class CRMReportControllerTest { + + private static final String AUTH = "test-session-key"; + private static final String WINDOW = + "{\"facilityID\":7,\"startDate\":\"2025-01-01T00:00:00.000\",\"endDate\":\"2025-01-31T23:59:00.000\"}"; + + @Mock + private CRMReportService crmReportService; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked report service") + void setUp() { + CRMReportController controller = new CRMReportController(); + ReflectionTestUtils.setField(controller, "crmReportService", crmReportService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + @DisplayName("getInwardStockReport should hand the posted window to the service and answer with the report") + void getInwardStockReport_shouldReturnReport() throws Exception { + when(crmReportService.getInwardStockReport(any(ItemStockEntryReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getInwardStockReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryReport.class); + verify(crmReportService).getInwardStockReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getInwardStockReport should report the failure when the report generation throws") + void getInwardStockReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getInwardStockReport(any(ItemStockEntryReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getInwardStockReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } + + @Test + @DisplayName("getExpiryReport should hand the posted window to the service and answer with the report") + void getExpiryReport_shouldReturnReport() throws Exception { + when(crmReportService.getExpiryReport(any(ItemStockEntryReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getExpiryReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryReport.class); + verify(crmReportService).getExpiryReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getExpiryReport should report the failure when the report generation throws") + void getExpiryReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getExpiryReport(any(ItemStockEntryReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getExpiryReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } + + @Test + @DisplayName("getConsumptionReport should hand the posted window to the service and answer with the report") + void getConsumptionReport_shouldReturnReport() throws Exception { + when(crmReportService.getConsumptionReport(any(ItemStockExitReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getConsumptionReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockExitReport.class); + verify(crmReportService).getConsumptionReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getConsumptionReport should report the failure when the report generation throws") + void getConsumptionReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getConsumptionReport(any(ItemStockExitReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getConsumptionReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } + + @Test + @DisplayName("getBenDrugIssueReport should hand the posted window to the service and answer with the report") + void getBenDrugIssueReport_shouldReturnReport() throws Exception { + when(crmReportService.getBenDrugIssueReport(any(PatientIssueExitReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getBenDrugIssueReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PatientIssueExitReport.class); + verify(crmReportService).getBenDrugIssueReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getBenDrugIssueReport should report the failure when the report generation throws") + void getBenDrugIssueReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getBenDrugIssueReport(any(PatientIssueExitReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getBenDrugIssueReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } + + @Test + @DisplayName("getDailyStockDetailReport should hand the posted window to the service and answer with the report") + void getDailyStockDetailReport_shouldReturnReport() throws Exception { + when(crmReportService.getDailyStockDetailsReport(any(ItemStockEntryReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getDailyStockDetailReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryReport.class); + verify(crmReportService).getDailyStockDetailsReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getDailyStockDetailReport should report the failure when the report generation throws") + void getDailyStockDetailReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getDailyStockDetailsReport(any(ItemStockEntryReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getDailyStockDetailReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } + + @Test + @DisplayName("getDailyStockSummaryReport should hand the posted window to the service and answer with the report") + void getDailyStockSummaryReport_shouldReturnReport() throws Exception { + when(crmReportService.getDailyStockSummaryReport(any(ItemStockEntryReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getDailyStockSummaryReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryReport.class); + verify(crmReportService).getDailyStockSummaryReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getDailyStockSummaryReport should report the failure when the report generation throws") + void getDailyStockSummaryReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getDailyStockSummaryReport(any(ItemStockEntryReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getDailyStockSummaryReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } + + @Test + @DisplayName("getMonthlyReport should hand the posted window to the service and answer with the report") + void getMonthlyReport_shouldReturnReport() throws Exception { + when(crmReportService.getMonthlyReport(any(ItemStockEntryReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getMonthlyReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryReport.class); + verify(crmReportService).getMonthlyReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getMonthlyReport should report the failure when the report generation throws") + void getMonthlyReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getMonthlyReport(any(ItemStockEntryReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getMonthlyReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } + + @Test + @DisplayName("getYearlyReport should hand the posted window to the service and answer with the report") + void getYearlyReport_shouldReturnReport() throws Exception { + when(crmReportService.getYearlyReport(any(ItemStockEntryReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getYearlyReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryReport.class); + verify(crmReportService).getYearlyReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getYearlyReport should report the failure when the report generation throws") + void getYearlyReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getYearlyReport(any(ItemStockEntryReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getYearlyReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } + + @Test + @DisplayName("getShortExpiryReport should hand the posted window to the service and answer with the report") + void getShortExpiryReport_shouldReturnReport() throws Exception { + when(crmReportService.getShortExpiryReport(any(ItemStockEntryReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getShortExpiryReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryReport.class); + verify(crmReportService).getShortExpiryReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getShortExpiryReport should report the failure when the report generation throws") + void getShortExpiryReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getShortExpiryReport(any(ItemStockEntryReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getShortExpiryReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } + + @Test + @DisplayName("getTransitReport should hand the posted window to the service and answer with the report") + void getTransitReport_shouldReturnReport() throws Exception { + when(crmReportService.getTransitReport(any(ItemStockEntryReport.class))).thenReturn("[{\"slNo\":1}]"); + + mockMvc.perform(post("/crmReportController/getTransitReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].slNo").value(1)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryReport.class); + verify(crmReportService).getTransitReport(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getTransitReport should report the failure when the report generation throws") + void getTransitReport_shouldReportServiceFailure() throws Exception { + when(crmReportService.getTransitReport(any(ItemStockEntryReport.class))).thenThrow(new RuntimeException("report failed")); + + mockMvc.perform(post("/crmReportController/getTransitReport").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("report failed")); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/stockEntry/StockEntryControllerTest.java b/src/test/java/com/iemr/inventory/controller/stockEntry/StockEntryControllerTest.java new file mode 100644 index 00000000..97351f4f --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/stockEntry/StockEntryControllerTest.java @@ -0,0 +1,300 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.stockEntry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.stockExit.ItemStockExit; +import com.iemr.inventory.data.stockentry.AllocateItemMap; +import com.iemr.inventory.data.stockentry.ItemMasterWithQuantityMap; +import com.iemr.inventory.data.stockentry.ItemStockEntry; +import com.iemr.inventory.data.stockentry.ItemStockEntryinput; +import com.iemr.inventory.data.stockentry.PhysicalStockEntry; +import com.iemr.inventory.service.stockEntry.StockEntryServiceImpl; +import com.iemr.inventory.utils.exception.InventoryException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("StockEntryController Test Suite") +class StockEntryControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private StockEntryServiceImpl stockEntryService; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked stock entry service") + void setUp() { + StockEntryController controller = new StockEntryController(); + ReflectionTestUtils.setField(controller, "stockEntryService", stockEntryService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static PhysicalStockEntry physical() { + PhysicalStockEntry physical = new PhysicalStockEntry(); + physical.setPhyEntryID(77L); + physical.setFacilityID(7); + return physical; + } + + private static ItemStockEntry batch() { + ItemStockEntry entry = new ItemStockEntry(); + entry.setItemStockEntryID(601); + entry.setBatchNo("B-1"); + return entry; + } + + @Test + @DisplayName("physicalStockEntry should book the posted stock in and answer with the saved header") + void physicalStockEntry_shouldBookStockIn() throws Exception { + when(stockEntryService.savePhysicalStockEntry(any(PhysicalStockEntry.class))).thenReturn(physical()); + + mockMvc.perform(post("/physicalStockEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7,\"itemStockEntry\":[]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PhysicalStockEntry.class); + verify(stockEntryService).savePhysicalStockEntry(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("physicalStockEntry should surface the duplicate-batch refusal from the service") + void physicalStockEntry_shouldSurfaceDuplicateRefusal() throws Exception { + when(stockEntryService.savePhysicalStockEntry(any(PhysicalStockEntry.class))) + .thenThrow(new InventoryException("Duplicate stock entry: Item ID 11 with batch 'B-1' already exists")); + + mockMvc.perform(post("/physicalStockEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5010)); + } + + @Test + @DisplayName("getItemBatchForStoreID should pass the posted probe batch to the service") + void getItemBatchForStoreID_shouldPassProbeThrough() throws Exception { + when(stockEntryService.getItemBatchForStoreID(any(ItemStockEntry.class))).thenReturn(List.of(batch())); + + mockMvc.perform(post("/getItemBatchForStoreID").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7,\"itemID\":11}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntry.class); + verify(stockEntryService).getItemBatchForStoreID(captor.capture()); + assertEquals(11, captor.getValue().getItemID()); + } + + @Test + @DisplayName("getItemBatchForStoreID should report the failure when the lookup throws") + void getItemBatchForStoreID_shouldReportServiceFailure() throws Exception { + when(stockEntryService.getItemBatchForStoreID(any(ItemStockEntry.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getItemBatchForStoreID").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("allocateStockFromItemID should allocate for the facility named in the path") + void allocateStockFromItemID_shouldAllocateForPathFacility() throws Exception { + when(stockEntryService.getItemStockFromItemID(anyInt(), anyList())) + .thenReturn(List.of(new AllocateItemMap())); + + mockMvc.perform(post("/allocateStockFromItemID/7").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("[{\"itemID\":11,\"quantity\":6}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(stockEntryService).getItemStockFromItemID(anyInt(), captor.capture()); + assertEquals(11, captor.getValue().get(0).getItemID()); + } + + @Test + @DisplayName("allocateStockFromItemID should report the failure when the allocation throws") + void allocateStockFromItemID_shouldReportServiceFailure() throws Exception { + when(stockEntryService.getItemStockFromItemID(anyInt(), anyList())) + .thenThrow(new InventoryException("no stock")); + + mockMvc.perform(post("/allocateStockFromItemID/7").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5010)); + } + + @Test + @DisplayName("getPhysicalStockEntry should return the entries in the posted window") + void getPhysicalStockEntry_shouldReturnEntriesInWindow() throws Exception { + when(stockEntryService.getPhysicalStockEntry(any(ItemStockEntryinput.class))) + .thenReturn(List.of(physical())); + + mockMvc.perform(post("/getPhysicalStockEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7,\"fromDate\":1735706400000,\"toDate\":1738298400000}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getPhysicalStockEntry should report the failure when the lookup throws") + void getPhysicalStockEntry_shouldReportServiceFailure() throws Exception { + when(stockEntryService.getPhysicalStockEntry(any(ItemStockEntryinput.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getPhysicalStockEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("itemPartialSearch should pass the item name and facility from the posted probe item") + void itemPartialSearch_shouldPassNameAndFacility() throws Exception { + when(stockEntryService.getItemMastersPartialSearch("Para", 7)).thenReturn(List.of(batch())); + + mockMvc.perform(post("/itemBatchPartialSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemName\":\"Para\",\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(stockEntryService).getItemMastersPartialSearch("Para", 7); + } + + @Test + @DisplayName("itemPartialSearch should report the failure when the search throws") + void itemPartialSearch_shouldReportServiceFailure() throws Exception { + when(stockEntryService.getItemMastersPartialSearch(anyString(), anyInt())) + .thenThrow(new RuntimeException("search failed")); + + mockMvc.perform(post("/itemBatchPartialSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemName\":\"Para\",\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("itemBatchWithZeroPartialSearch should include the batches that hold no stock left") + void itemBatchWithZeroPartialSearch_shouldIncludeEmptyBatches() throws Exception { + when(stockEntryService.getItemMastersPartialSearchWithZero("Para", 7)).thenReturn(List.of(batch())); + + mockMvc.perform(post("/itemBatchWithZeroPartialSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemName\":\"Para\",\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(stockEntryService).getItemMastersPartialSearchWithZero("Para", 7); + } + + @Test + @DisplayName("itemBatchWithZeroPartialSearch should report the failure when the search throws") + void itemBatchWithZeroPartialSearch_shouldReportServiceFailure() throws Exception { + when(stockEntryService.getItemMastersPartialSearchWithZero(anyString(), anyInt())) + .thenThrow(new RuntimeException("search failed")); + + mockMvc.perform(post("/itemBatchWithZeroPartialSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemName\":\"Para\",\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getPhysicalStockEntryItems should load the batches booked under the posted header id") + void getPhysicalStockEntryItems_shouldLoadBatchesOfHeader() throws Exception { + when(stockEntryService.getPhysicalStockEntryItems(77L)).thenReturn(List.of(batch())); + + mockMvc.perform(post("/getPhysicalStockEntryItems").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"phyEntryID\":77}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(stockEntryService).getPhysicalStockEntryItems(77L); + } + + @Test + @DisplayName("getPhysicalStockEntryItems should report the failure when the header is missing") + void getPhysicalStockEntryItems_shouldReportFailureWhenHeaderMissing() throws Exception { + when(stockEntryService.getPhysicalStockEntryItems(anyLong())) + .thenThrow(new java.util.NoSuchElementException("No value present")); + + mockMvc.perform(post("/getPhysicalStockEntryItems").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"phyEntryID\":77}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getItemwithQuantityPartialSearch should pass the item name and facility from the posted probe") + void getItemwithQuantityPartialSearch_shouldPassNameAndFacility() throws Exception { + when(stockEntryService.getItemwithQuantityPartialSearch("Para", 7)) + .thenReturn(List.of(new ItemMasterWithQuantityMap())); + + mockMvc.perform(post("/getItemwithQuantityPartialSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemName\":\"Para\",\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(stockEntryService).getItemwithQuantityPartialSearch("Para", 7); + } + + @Test + @DisplayName("getItemwithQuantityPartialSearch should report the failure when the search throws") + void getItemwithQuantityPartialSearch_shouldReportServiceFailure() throws Exception { + when(stockEntryService.getItemwithQuantityPartialSearch(anyString(), anyInt())) + .thenThrow(new RuntimeException("search failed")); + + mockMvc.perform(post("/getItemwithQuantityPartialSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"itemName\":\"Para\",\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/stockExit/StockExitControllerTest.java b/src/test/java/com/iemr/inventory/controller/stockExit/StockExitControllerTest.java new file mode 100644 index 00000000..839226fd --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/stockExit/StockExitControllerTest.java @@ -0,0 +1,334 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.stockExit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.stockExit.ItemStockExitMap; +import com.iemr.inventory.data.stockExit.StoreSelfConsumption; +import com.iemr.inventory.data.stockExit.T_PatientIssue; +import com.iemr.inventory.data.stockExit.T_StockTransfer; +import com.iemr.inventory.data.stockentry.ItemStockEntryinput; +import com.iemr.inventory.service.stockExit.StockExitServiceImpl; +import com.iemr.inventory.utils.exception.InventoryException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("StockExitController Test Suite") +class StockExitControllerTest { + + private static final String AUTH = "test-session-key"; + private static final String WINDOW = + "{\"facilityID\":7,\"fromDate\":1735706400000,\"toDate\":1738298400000}"; + + @Mock + private StockExitServiceImpl stockExitService; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked stock exit service") + void setUp() { + StockExitController controller = new StockExitController(); + ReflectionTestUtils.setField(controller, "stockExitService", stockExitService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + @DisplayName("patientIssue should confirm the dispense when the service books it") + void patientIssue_shouldConfirmDispense() throws Exception { + when(stockExitService.issuePatientDrugs(any(T_PatientIssue.class))).thenReturn(1); + + mockMvc.perform(post("/patientIssue").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7,\"benRegID\":101,\"itemStockExit\":[]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.response").value("Successfully Created")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(T_PatientIssue.class); + verify(stockExitService).issuePatientDrugs(captor.capture()); + assertEquals(101L, captor.getValue().getBenRegID()); + } + + @Test + @DisplayName("patientIssue should report a failure when the service books nothing") + void patientIssue_shouldReportFailureWhenNothingBooked() throws Exception { + when(stockExitService.issuePatientDrugs(any(T_PatientIssue.class))).thenReturn(0); + + mockMvc.perform(post("/patientIssue").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("Error occured while saving the request")); + } + + @Test + @DisplayName("patientIssue should surface the inventory failure raised by the service") + void patientIssue_shouldSurfaceInventoryFailure() throws Exception { + when(stockExitService.issuePatientDrugs(any(T_PatientIssue.class))) + .thenThrow(new InventoryException("No item found to dispense.")); + + mockMvc.perform(post("/patientIssue").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5010)); + } + + @Test + @DisplayName("storeSelfConsumption should confirm the consumption when the service books it") + void storeSelfConsumption_shouldConfirmConsumption() throws Exception { + when(stockExitService.storeSelfConsumption(any(StoreSelfConsumption.class))).thenReturn(1); + + mockMvc.perform(post("/storeSelfConsumption").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7,\"itemStockExit\":[]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.response").value("Successfully Created")); + } + + @Test + @DisplayName("storeSelfConsumption should report a failure when the service books nothing") + void storeSelfConsumption_shouldReportFailureWhenNothingBooked() throws Exception { + when(stockExitService.storeSelfConsumption(any(StoreSelfConsumption.class))).thenReturn(0); + + mockMvc.perform(post("/storeSelfConsumption").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("storeTransfer should confirm the transfer when the service books it") + void storeTransfer_shouldConfirmTransfer() throws Exception { + when(stockExitService.storeTransfer(any(T_StockTransfer.class))).thenReturn(1); + + mockMvc.perform(post("/storeTransfer").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"transferFromFacilityID\":1,\"transferToFacilityID\":2,\"itemStockExit\":[]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.response").value("Successfully Created")); + } + + @Test + @DisplayName("storeTransfer should report a failure when the service books nothing") + void storeTransfer_shouldReportFailureWhenNothingBooked() throws Exception { + when(stockExitService.storeTransfer(any(T_StockTransfer.class))).thenReturn(0); + + mockMvc.perform(post("/storeTransfer").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"transferFromFacilityID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getPatientissue should return the patient issues in the posted window") + void getPatientissue_shouldReturnIssuesInWindow() throws Exception { + when(stockExitService.getpatientIssue(any(ItemStockEntryinput.class))) + .thenReturn(List.of(new T_PatientIssue())); + + mockMvc.perform(post("/getPatientissue").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getPatientissue should report the failure when the lookup throws") + void getPatientissue_shouldReportServiceFailure() throws Exception { + when(stockExitService.getpatientIssue(any(ItemStockEntryinput.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getPatientissue").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getSelfConsumption should return the consumptions in the posted window") + void getSelfConsumption_shouldReturnConsumptionsInWindow() throws Exception { + when(stockExitService.getstoreSelfConsumption(any(ItemStockEntryinput.class))) + .thenReturn(List.of(new StoreSelfConsumption())); + + mockMvc.perform(post("/getSelfConsumption").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getSelfConsumption should report the failure when the lookup throws") + void getSelfConsumption_shouldReportServiceFailure() throws Exception { + when(stockExitService.getstoreSelfConsumption(any(ItemStockEntryinput.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getSelfConsumption").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getStoreTransfer should return the transfers in the posted window") + void getStoreTransfer_shouldReturnTransfersInWindow() throws Exception { + when(stockExitService.getStoreTransfer(any(ItemStockEntryinput.class))) + .thenReturn(List.of(new T_StockTransfer())); + + mockMvc.perform(post("/getStoreTransfer").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getStoreTransfer should report the failure when the lookup throws") + void getStoreTransfer_shouldReportServiceFailure() throws Exception { + when(stockExitService.getStoreTransfer(any(ItemStockEntryinput.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getStoreTransfer").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getPatientissueAllDetail should load the issue named by the posted id") + void getPatientissueAllDetail_shouldLoadIssueById() throws Exception { + when(stockExitService.getPatientissueAllDetail(88L)).thenReturn(new T_PatientIssue()); + + mockMvc.perform(post("/getPatientissueAllDetail").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"patientIssueID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(stockExitService).getPatientissueAllDetail(88L); + } + + @Test + @DisplayName("getPatientissueAllDetail should report the failure when the issue is missing") + void getPatientissueAllDetail_shouldReportFailureWhenIssueMissing() throws Exception { + when(stockExitService.getPatientissueAllDetail(anyLong())).thenReturn(null); + + mockMvc.perform(post("/getPatientissueAllDetail").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"patientIssueID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("getPatientissueItemEntry should project the lines booked under the posted issue") + void getPatientissueItemEntry_shouldProjectBookedLines() throws Exception { + when(stockExitService.getpatientIssueItemLIst(any(ItemStockEntryinput.class))) + .thenReturn(List.of(new ItemStockExitMap())); + + mockMvc.perform(post("/getPatientissueItemEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"patientIssueID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getPatientissueItemEntry should report the failure when the lookup throws") + void getPatientissueItemEntry_shouldReportServiceFailure() throws Exception { + when(stockExitService.getpatientIssueItemLIst(any(ItemStockEntryinput.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getPatientissueItemEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"patientIssueID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getSelfConsumptionItemEntry should project the lines booked under the posted consumption") + void getSelfConsumptionItemEntry_shouldProjectBookedLines() throws Exception { + when(stockExitService.getstoreSelfConsumptionItemList(any(ItemStockEntryinput.class))) + .thenReturn(List.of(new ItemStockExitMap())); + + mockMvc.perform(post("/getSelfConsumptionItemEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"consumptionID\":66}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryinput.class); + verify(stockExitService).getstoreSelfConsumptionItemList(captor.capture()); + assertEquals(66L, captor.getValue().getConsumptionID()); + } + + @Test + @DisplayName("getSelfConsumptionItemEntry should report the failure when the lookup throws") + void getSelfConsumptionItemEntry_shouldReportServiceFailure() throws Exception { + when(stockExitService.getstoreSelfConsumptionItemList(any(ItemStockEntryinput.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getSelfConsumptionItemEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"consumptionID\":66}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getStoreTransferItemEntry should project the batches booked under the posted transfer") + void getStoreTransferItemEntry_shouldProjectBookedBatches() throws Exception { + when(stockExitService.getStoreTransferItemEntry(any(ItemStockEntryinput.class))) + .thenReturn(List.of(new ItemStockExitMap())); + + mockMvc.perform(post("/getStoreTransferItemEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"stockTransferID\":99}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getStoreTransferItemEntry should report the failure when the lookup throws") + void getStoreTransferItemEntry_shouldReportServiceFailure() throws Exception { + when(stockExitService.getStoreTransferItemEntry(any(ItemStockEntryinput.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getStoreTransferItemEntry").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"stockTransferID\":99}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/store/StockAdjustmentControllerTest.java b/src/test/java/com/iemr/inventory/controller/store/StockAdjustmentControllerTest.java new file mode 100644 index 00000000..f470359a --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/store/StockAdjustmentControllerTest.java @@ -0,0 +1,239 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.store; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.stockadjustment.StockAdjustment; +import com.iemr.inventory.data.stockadjustment.StockAdjustmentDraft; +import com.iemr.inventory.data.stockentry.ItemStockEntryinput; +import com.iemr.inventory.service.stockadjustment.StockAdjustmentServiceImpl; +import com.iemr.inventory.utils.exception.InventoryException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("StockAdjustmentController Test Suite") +class StockAdjustmentControllerTest { + + private static final String AUTH = "test-session-key"; + private static final String WINDOW = + "{\"facilityID\":7,\"fromDate\":1735706400000,\"toDate\":1738298400000}"; + + @Mock + private StockAdjustmentServiceImpl stockAdjustmentServiceImpl; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked stock adjustment service") + void setUp() { + StockAdjustmentController controller = new StockAdjustmentController(); + ReflectionTestUtils.setField(controller, "stockAdjustmentServiceImpl", stockAdjustmentServiceImpl); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static StockAdjustmentDraft draft(Long id) { + StockAdjustmentDraft draft = new StockAdjustmentDraft(); + draft.setStockAdjustmentDraftID(id); + draft.setFacilityID(7); + return draft; + } + + private static StockAdjustment adjustment(Long id) { + StockAdjustment adjustment = new StockAdjustment(); + adjustment.setStockAdjustmentID(id); + adjustment.setFacilityID(7); + return adjustment; + } + + @Test + @DisplayName("stockadjustmentdraft should save the posted draft and answer with the stored row") + void stockadjustmentdraft_shouldSavePostedDraft() throws Exception { + when(stockAdjustmentServiceImpl.saveDraft(any(StockAdjustmentDraft.class))).thenReturn(draft(55L)); + + mockMvc.perform(post("/stockadjustmentdraft").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7,\"draftName\":\"a name\",\"stockAdjustmentItemDraft\":[]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StockAdjustmentDraft.class); + verify(stockAdjustmentServiceImpl).saveDraft(captor.capture()); + assertEquals("a name", captor.getValue().getDraftName()); + } + + @Test + @DisplayName("stockadjustmentdraft should report the failure when the save throws") + void stockadjustmentdraft_shouldReportServiceFailure() throws Exception { + when(stockAdjustmentServiceImpl.saveDraft(any(StockAdjustmentDraft.class))) + .thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/stockadjustmentdraft").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getstockadjustmentdraftTransaction should return the drafts in the posted window") + void getstockadjustmentdraftTransaction_shouldReturnDraftsInWindow() throws Exception { + when(stockAdjustmentServiceImpl.getStockAjustmentDraftTransaction(any(ItemStockEntryinput.class))) + .thenReturn(List.of(draft(55L))); + + mockMvc.perform(post("/getstockadjustmentdraftTransaction").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockEntryinput.class); + verify(stockAdjustmentServiceImpl).getStockAjustmentDraftTransaction(captor.capture()); + assertEquals(7, captor.getValue().getFacilityID()); + } + + @Test + @DisplayName("getstockadjustmentdraftTransaction should report the failure when the lookup throws") + void getstockadjustmentdraftTransaction_shouldReportServiceFailure() throws Exception { + when(stockAdjustmentServiceImpl.getStockAjustmentDraftTransaction(any(ItemStockEntryinput.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getstockadjustmentdraftTransaction").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getforEditsSockAdjustmentdraftTransaction should load the draft named by the posted id") + void getforEditsSockAdjustmentdraftTransaction_shouldLoadDraftById() throws Exception { + when(stockAdjustmentServiceImpl.getforeditStockAjustmentDraftTransaction(55L)).thenReturn(draft(55L)); + + mockMvc.perform(post("/getforEditsStockAdjustmentdraftTransaction").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"stockAdjustmentDraftID\":55}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(stockAdjustmentServiceImpl).getforeditStockAjustmentDraftTransaction(55L); + } + + @Test + @DisplayName("getforEditsSockAdjustmentdraftTransaction should report the failure when the draft is missing") + void getforEditsSockAdjustmentdraftTransaction_shouldReportFailureWhenDraftMissing() throws Exception { + when(stockAdjustmentServiceImpl.getforeditStockAjustmentDraftTransaction(anyLong())).thenReturn(null); + + mockMvc.perform(post("/getforEditsStockAdjustmentdraftTransaction").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"stockAdjustmentDraftID\":55}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("stockadjustment should post the adjustment and answer with the stored row") + void stockadjustment_shouldPostAdjustment() throws Exception { + when(stockAdjustmentServiceImpl.savetransaction(any(StockAdjustment.class))).thenReturn(adjustment(88L)); + + mockMvc.perform(post("/stockadjustment").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7,\"stockAdjustmentItem\":[]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("stockadjustment should surface the inventory failure raised by the service") + void stockadjustment_shouldSurfaceInventoryFailure() throws Exception { + when(stockAdjustmentServiceImpl.savetransaction(any(StockAdjustment.class))) + .thenThrow(new InventoryException("Adjustment Quantity for issue should be more than available quantity")); + + mockMvc.perform(post("/stockadjustment").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5010)); + } + + @Test + @DisplayName("getforeditStockAdjustmentTransaction should return the adjustments in the posted window") + void getforeditStockAdjustmentTransaction_shouldReturnAdjustmentsInWindow() throws Exception { + when(stockAdjustmentServiceImpl.getStockAjustmentTransaction(any(ItemStockEntryinput.class))) + .thenReturn(List.of(adjustment(88L))); + + mockMvc.perform(post("/getStockAdjustmentTransaction").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getforeditStockAdjustmentTransaction should report the failure when the lookup throws") + void getforeditStockAdjustmentTransaction_shouldReportServiceFailure() throws Exception { + when(stockAdjustmentServiceImpl.getStockAjustmentTransaction(any(ItemStockEntryinput.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getStockAdjustmentTransaction").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content(WINDOW)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getforEditsStockAdjustmentTransaction should load the adjustment named by the posted id") + void getforEditsStockAdjustmentTransaction_shouldLoadAdjustmentById() throws Exception { + when(stockAdjustmentServiceImpl.getforeditStockAjustmentTransaction(88L)).thenReturn(adjustment(88L)); + + mockMvc.perform(post("/getforEditsStockAdjustmentTransaction").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"stockAdjustmentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(stockAdjustmentServiceImpl).getforeditStockAjustmentTransaction(88L); + } + + @Test + @DisplayName("getforEditsStockAdjustmentTransaction should report the failure when the adjustment is missing") + void getforEditsStockAdjustmentTransaction_shouldReportFailureWhenAdjustmentMissing() throws Exception { + when(stockAdjustmentServiceImpl.getforeditStockAjustmentTransaction(anyLong())).thenReturn(null); + + mockMvc.perform(post("/getforEditsStockAdjustmentTransaction").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{\"stockAdjustmentID\":88}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/store/StoreControllerTest.java b/src/test/java/com/iemr/inventory/controller/store/StoreControllerTest.java new file mode 100644 index 00000000..9ae662d9 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/store/StoreControllerTest.java @@ -0,0 +1,323 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.store; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.store.M_Facility; +import com.iemr.inventory.data.store.M_Van; +import com.iemr.inventory.service.store.StoreService; +import com.iemr.inventory.utils.exception.IEMRException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("StoreController Test Suite") +class StoreControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private StoreService storeService; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked store service") + void setUp() { + StoreController controller = new StoreController(); + ReflectionTestUtils.setField(controller, "storeService", storeService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static M_Facility facility(Integer id, String name) { + M_Facility facility = new M_Facility(); + facility.setFacilityID(id); + facility.setFacilityName(name); + facility.setProviderServiceMapID(3); + return facility; + } + + @Test + @DisplayName("createStore should save the posted batch of stores") + void createStore_shouldSavePostedBatch() throws Exception { + when(storeService.addAllMainStore(anyList())).thenReturn(List.of(facility(7, "Main store"))); + + mockMvc.perform(post("/createStore").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{\"facilityName\":\"Main store\",\"providerServiceMapID\":3}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(storeService).addAllMainStore(captor.capture()); + assertEquals(1, captor.getValue().size()); + } + + @Test + @DisplayName("createStore should report the failure when the save throws") + void createStore_shouldReportServiceFailure() throws Exception { + when(storeService.addAllMainStore(anyList())).thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/createStore").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("editStore should copy the description and modifier onto the stored facility before saving") + void editStore_shouldCopyEditableFields() throws Exception { + M_Facility stored = facility(7, "Main store"); + when(storeService.getMainStore(7)).thenReturn(stored); + when(storeService.createMainStore(any(M_Facility.class))).thenAnswer(inv -> inv.getArgument(0)); + + mockMvc.perform(post("/editStore").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7,\"facilityDesc\":\"new desc\",\"modifiedBy\":\"tester\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Facility.class); + verify(storeService).createMainStore(captor.capture()); + assertEquals("new desc", captor.getValue().getFacilityDesc()); + assertEquals("tester", captor.getValue().getModifiedBy()); + } + + @Test + @DisplayName("editStore should report the failure when the store cannot be found") + void editStore_shouldReportFailureWhenStoreMissing() throws Exception { + when(storeService.getMainStore(7)).thenReturn(null); + + mockMvc.perform(post("/editStore").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("getAllStore should return every store of the provider service map named in the path") + void getAllStore_shouldReturnStoresOfProviderServiceMap() throws Exception { + when(storeService.getAllMainStore(3)).thenReturn(List.of(facility(7, "Main store"))); + + mockMvc.perform(post("/getAllStore/3").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(storeService).getAllMainStore(3); + } + + @Test + @DisplayName("getAllStore should report the failure when the lookup throws") + void getAllStore_shouldReportServiceFailure() throws Exception { + when(storeService.getAllMainStore(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getAllStore/3").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getAllActiveStore should pass the posted probe facility straight to the service") + void getAllActiveStore_shouldPassProbeThrough() throws Exception { + when(storeService.getAllActiveStore(any(M_Facility.class))) + .thenReturn(List.of(facility(7, "Main store"))); + + mockMvc.perform(post("/getAllActiveStore").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3,\"deleted\":false}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Facility.class); + verify(storeService).getAllActiveStore(captor.capture()); + assertEquals(Boolean.FALSE, captor.getValue().getDeleted()); + } + + @Test + @DisplayName("getAllActiveStore should report the failure when the lookup throws") + void getAllActiveStore_shouldReportServiceFailure() throws Exception { + when(storeService.getAllActiveStore(any(M_Facility.class))) + .thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getAllActiveStore").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getMainFacility should pass the provider service map and the main-facility flag through") + void getMainFacility_shouldPassFlagThrough() throws Exception { + when(storeService.getMainFacility(3, true)) + .thenReturn(new ArrayList<>(List.of(facility(7, "Main store")))); + + mockMvc.perform(post("/getMainFacility").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3,\"isMainFacility\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(storeService).getMainFacility(3, true); + } + + @Test + @DisplayName("getMainFacility should report the failure when the lookup throws") + void getMainFacility_shouldReportServiceFailure() throws Exception { + when(storeService.getMainFacility(anyInt(), anyBoolean())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getMainFacility").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3,\"isMainFacility\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getsubFacility should look the sub-stores up under the posted parent facility") + void getsubFacility_shouldLookUpUnderParent() throws Exception { + when(storeService.getChildFacility(3, 7)) + .thenReturn(new ArrayList<>(List.of(facility(8, "Sub store")))); + + mockMvc.perform(post("/getsubFacility").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3,\"mainFacilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(storeService).getChildFacility(3, 7); + } + + @Test + @DisplayName("getsubFacility should report the failure when the lookup throws") + void getsubFacility_shouldReportServiceFailure() throws Exception { + when(storeService.getChildFacility(anyInt(), anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getsubFacility").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3,\"mainFacilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("deleteStore should answer with the facility the service deactivated") + void deleteStore_shouldReturnDeactivatedFacility() throws Exception { + when(storeService.deleteStore(any(M_Facility.class))).thenReturn(facility(7, "Main store")); + + mockMvc.perform(post("/deleteStore").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7,\"deleted\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("deleteStore should answer 200 with the refusal text when the service rejects the request") + void deleteStore_shouldReturnRefusalTextAsSuccessfulResponse() throws Exception { + when(storeService.deleteStore(any(M_Facility.class))) + .thenThrow(new IEMRException("Child Stores are still active")); + + mockMvc.perform(post("/deleteStore").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7,\"deleted\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.response").value("Child Stores are still active")); + } + + @Test + @DisplayName("getStoreByID should return the store behind the posted facility id") + void getStoreByID_shouldReturnStore() throws Exception { + when(storeService.getStoreByID(7)).thenReturn(facility(7, "Main store")); + + mockMvc.perform(post("/getStoreByID").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(storeService).getStoreByID(7); + } + + @Test + @DisplayName("getStoreByID should answer 200 with the failure text when the lookup throws") + void getStoreByID_shouldReturnFailureTextAsSuccessfulResponse() throws Exception { + when(storeService.getStoreByID(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getStoreByID").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"facilityID\":7}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getVanByStoreID should return the van attached to the store") + void getVanByStoreID_shouldReturnVan() throws Exception { + M_Van van = new M_Van(); + van.setVanID(4); + when(storeService.getVanByStoreID(7)).thenReturn(van); + + mockMvc.perform(post("/getVanByStoreID/7").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(storeService).getVanByStoreID(7); + } + + @Test + @DisplayName("getVanByStoreID should answer with an empty van when the store has none attached") + void getVanByStoreID_shouldReturnEmptyVanWhenNoneAttached() throws Exception { + when(storeService.getVanByStoreID(7)).thenReturn(null); + + mockMvc.perform(post("/getVanByStoreID/7").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + } + + @Test + @DisplayName("getVanByStoreID should report the failure when the lookup throws") + void getVanByStoreID_shouldReportServiceFailure() throws Exception { + when(storeService.getVanByStoreID(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getVanByStoreID/7").header("Authorization", AUTH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/uom/UnitOfMeasurementControllerTest.java b/src/test/java/com/iemr/inventory/controller/uom/UnitOfMeasurementControllerTest.java new file mode 100644 index 00000000..6db8dc98 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/uom/UnitOfMeasurementControllerTest.java @@ -0,0 +1,204 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.uom; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.uom.M_Uom; +import com.iemr.inventory.service.uom.UomInter; + +@ExtendWith(MockitoExtension.class) +@DisplayName("UnitOfMeasurementController Test Suite") +class UnitOfMeasurementControllerTest { + + @Mock + private UomInter uomInter; + + private static final String AUTH = "test-session-key"; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked UoM service") + void setUp() { + UnitOfMeasurementController controller = new UnitOfMeasurementController(); + ReflectionTestUtils.setField(controller, "uomInter", uomInter); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static M_Uom uom(Integer id, String name) { + M_Uom uom = new M_Uom(); + uom.setuOMID(id); + uom.setuOMName(name); + uom.setuOMDesc("a description"); + uom.setuOMCode("CODE"); + uom.setStatus("Active"); + uom.setProviderServiceMapID(3); + return uom; + } + + private static ArrayList uomList(M_Uom... items) { + return new ArrayList<>(List.of(items)); + } + + @Test + @DisplayName("createUom should persist the posted array and answer with the saved rows") + void createUom_shouldPersistPostedArray() throws Exception { + when(uomInter.createDrugtypeData(anyList())).thenReturn(uomList(uom(1, "Tablet"))); + + mockMvc.perform(post("/createUom").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("[{\"uOMName\":\"Tablet\",\"providerServiceMapID\":3}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data[0].uOMName").value("Tablet")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(uomInter).createDrugtypeData(captor.capture()); + org.junit.jupiter.api.Assertions.assertEquals(1, captor.getValue().size()); + } + + @Test + @DisplayName("createUom should report the failure when the service blows up") + void createUom_shouldReportServiceFailure() throws Exception { + when(uomInter.createDrugtypeData(anyList())).thenThrow(new RuntimeException("db unavailable")); + + mockMvc.perform(post("/createUom").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON).content("[{}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("db unavailable")); + } + + @Test + @DisplayName("createUom should report a parse failure for a malformed body") + void createUom_shouldReportParseFailure() throws Exception { + mockMvc.perform(post("/createUom").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON).content("not-json")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("getUom should look the rows up by the posted provider service map id") + void getUom_shouldLookUpByProviderServiceMapId() throws Exception { + when(uomInter.createDrugtypeData(3)).thenReturn(uomList(uom(1, "Tablet"), uom(2, "Syrup"))); + + mockMvc.perform(post("/getUom").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.length()").value(2)); + + verify(uomInter).createDrugtypeData(3); + } + + @Test + @DisplayName("getUom should report the failure when the lookup throws") + void getUom_shouldReportServiceFailure() throws Exception { + when(uomInter.createDrugtypeData(anyInt())).thenThrow(new RuntimeException("lookup failed")); + + mockMvc.perform(post("/getUom").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)); + } + + @Test + @DisplayName("editUom should copy the editable fields onto the stored row before saving") + void editUom_shouldCopyEditableFieldsBeforeSaving() throws Exception { + M_Uom stored = uom(1, "old name"); + when(uomInter.editDrugtypeData(1)).thenReturn(stored); + when(uomInter.saveeditedData(any(M_Uom.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/editUom").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"uomID\":1,\"uOMName\":\"new name\",\"uOMDesc\":\"new desc\"," + + "\"uOMCode\":\"NEW\",\"status\":\"Inactive\",\"modifiedBy\":\"tester\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)) + .andExpect(jsonPath("$.data.uOMName").value("new name")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Uom.class); + verify(uomInter).saveeditedData(captor.capture()); + M_Uom saved = captor.getValue(); + org.junit.jupiter.api.Assertions.assertEquals("new desc", saved.getuOMDesc()); + org.junit.jupiter.api.Assertions.assertEquals("NEW", saved.getuOMCode()); + org.junit.jupiter.api.Assertions.assertEquals("Inactive", saved.getStatus()); + org.junit.jupiter.api.Assertions.assertEquals("tester", saved.getModifiedBy()); + } + + @Test + @DisplayName("editUom should report the failure when the row cannot be found") + void editUom_shouldReportFailureWhenRowMissing() throws Exception { + when(uomInter.editDrugtypeData(1)).thenReturn(null); + + mockMvc.perform(post("/editUom").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON).content("{\"uomID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } + + @Test + @DisplayName("deleteUom should flip the deleted flag on the stored row and save it") + void deleteUom_shouldFlipDeletedFlag() throws Exception { + M_Uom stored = uom(1, "Tablet"); + when(uomInter.editDrugtypeData(1)).thenReturn(stored); + when(uomInter.saveeditedData(any(M_Uom.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + mockMvc.perform(post("/deleteUom").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON) + .content("{\"uomID\":1,\"deleted\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(M_Uom.class); + verify(uomInter).saveeditedData(captor.capture()); + org.junit.jupiter.api.Assertions.assertEquals(Boolean.TRUE, captor.getValue().getDeleted()); + } + + @Test + @DisplayName("deleteUom should report the failure when the row cannot be found") + void deleteUom_shouldReportFailureWhenRowMissing() throws Exception { + when(uomInter.editDrugtypeData(1)).thenReturn(null); + + mockMvc.perform(post("/deleteUom").header("Authorization", AUTH).contentType(MediaType.APPLICATION_JSON).content("{\"uomID\":1}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5005)); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/version/VersionControllerTest.java b/src/test/java/com/iemr/inventory/controller/version/VersionControllerTest.java new file mode 100644 index 00000000..b3c26821 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/version/VersionControllerTest.java @@ -0,0 +1,78 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.version; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +@DisplayName("VersionController Test Suite") +class VersionControllerTest { + + private MockMvc mockMvc; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + VersionController versionController = new VersionController(); + + mockMvc = MockMvcBuilders.standaloneSetup(versionController).build(); + + objectMapper = new ObjectMapper(); + } + + @Test + @DisplayName("Should return version details sourced from git.properties on the classpath") + void versionInformation_shouldReturnGitPropertiesContent() throws Exception { + Properties gitProperties = new Properties(); + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("git.properties")) { + if (inputStream == null) { + throw new IOException("git.properties file not found in test resources."); + } + gitProperties.load(inputStream); + } + + mockMvc.perform(get("/version")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.buildTimestamp").value(gitProperties.getProperty("git.build.time", "unknown"))) + .andExpect(jsonPath("$.version").value(gitProperties.getProperty("git.build.version", "unknown"))) + .andExpect(jsonPath("$.branch").value(gitProperties.getProperty("git.branch", "unknown"))) + .andExpect(jsonPath("$.commitHash").value(gitProperties.getProperty("git.commit.id.abbrev", "unknown"))); + } +} diff --git a/src/test/java/com/iemr/inventory/controller/visit/VisitControllerTest.java b/src/test/java/com/iemr/inventory/controller/visit/VisitControllerTest.java new file mode 100644 index 00000000..70396807 --- /dev/null +++ b/src/test/java/com/iemr/inventory/controller/visit/VisitControllerTest.java @@ -0,0 +1,143 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.controller.visit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +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 org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.iemr.inventory.data.visit.BeneficiaryModel; +import com.iemr.inventory.service.visit.VisitService; +import com.iemr.inventory.utils.exception.InventoryException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("VisitController Test Suite") +class VisitControllerTest { + + private static final String AUTH = "test-session-key"; + + @Mock + private VisitService visitService; + + private MockMvc mockMvc; + + @BeforeEach + @DisplayName("Stand the controller up with a mocked visit service") + void setUp() { + VisitController controller = new VisitController(); + ReflectionTestUtils.setField(controller, "visitService", visitService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private static BeneficiaryModel beneficiary(Long regID) { + BeneficiaryModel model = new BeneficiaryModel(); + model.setBeneficiaryRegID(regID); + return model; + } + + @Test + @DisplayName("getVisitFromBenRegID should forward the beneficiary id, provider service map and auth header") + void getVisitFromBenRegID_shouldForwardRequestDetails() throws Exception { + when(visitService.getVisitDetail(eq("12345"), eq(3), eq(AUTH))).thenReturn(beneficiary(77L)); + + mockMvc.perform(post("/getVisitFromBenID").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"beneficiaryID\":\"12345\",\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(visitService).getVisitDetail("12345", 3, AUTH); + } + + @Test + @DisplayName("getVisitFromBenRegID should report the failure when the beneficiary id is blank") + void getVisitFromBenRegID_shouldReportFailureForBlankBeneficiaryId() throws Exception { + when(visitService.getVisitDetail(eq(" "), anyInt(), anyString())).thenReturn(beneficiary(77L)); + + mockMvc.perform(post("/getVisitFromBenID").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"beneficiaryID\":\" \",\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("Beneficiary ID cannot be null or empty")); + } + + @Test + @DisplayName("getVisitFromBenRegID should report the inventory failure raised by the service") + void getVisitFromBenRegID_shouldReportInventoryFailure() throws Exception { + when(visitService.getVisitDetail(anyString(), anyInt(), anyString())) + .thenThrow(new InventoryException("Invalid Beneficiary ID")); + + mockMvc.perform(post("/getVisitFromBenID").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"beneficiaryID\":\"12345\",\"providerServiceMapID\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5010)) + .andExpect(jsonPath("$.errorMessage").value("Invalid Beneficiary ID")); + } + + @Test + @DisplayName("getVisitFromAdvanceSearch should forward the raw search payload and the auth header") + void getVisitFromAdvanceSearch_shouldForwardSearchPayload() throws Exception { + when(visitService.getVisitFromAdvanceSearch(anyString(), eq(AUTH))) + .thenReturn(List.of(beneficiary(77L))); + + mockMvc.perform(post("/getVisitFromAdvanceSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("12345")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(200)); + + verify(visitService).getVisitFromAdvanceSearch("12345", AUTH); + } + + @Test + @DisplayName("getVisitFromAdvanceSearch should report the failure when the search throws") + void getVisitFromAdvanceSearch_shouldReportServiceFailure() throws Exception { + when(visitService.getVisitFromAdvanceSearch(anyString(), any())) + .thenThrow(new RuntimeException("search unavailable")); + + mockMvc.perform(post("/getVisitFromAdvanceSearch").header("Authorization", AUTH) + .contentType(MediaType.APPLICATION_JSON).content("12345")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value(5000)) + .andExpect(jsonPath("$.errorMessage").value("search unavailable")); + } +} diff --git a/src/test/java/com/iemr/inventory/exceptionhandler/DataNotFoundTest.java b/src/test/java/com/iemr/inventory/exceptionhandler/DataNotFoundTest.java new file mode 100644 index 00000000..bf2b9df3 --- /dev/null +++ b/src/test/java/com/iemr/inventory/exceptionhandler/DataNotFoundTest.java @@ -0,0 +1,41 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.exceptionhandler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("DataNotFound Test Suite") +class DataNotFoundTest { + + @Test + @DisplayName("DataNotFound should be an unchecked exception carrying the supplied message") + void dataNotFound_shouldCarryMessage() { + DataNotFound exception = new DataNotFound("no such item"); + + assertEquals("no such item", exception.getMessage()); + assertInstanceOf(RuntimeException.class, exception); + } +} diff --git a/src/test/java/com/iemr/inventory/mapper/report/InventoryReportMapperTest.java b/src/test/java/com/iemr/inventory/mapper/report/InventoryReportMapperTest.java new file mode 100644 index 00000000..18d46cd2 --- /dev/null +++ b/src/test/java/com/iemr/inventory/mapper/report/InventoryReportMapperTest.java @@ -0,0 +1,143 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.mapper.report; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.sql.Date; +import java.sql.Timestamp; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.inventory.data.report.ItemStockEntryReport; +import com.iemr.inventory.data.report.PatientIssueExitReport; +import com.iemr.inventory.model.report.BenDrugIssueReport; +import com.iemr.inventory.model.report.ExpiryReport; +import com.iemr.inventory.model.report.InwardStockReport; + +@DisplayName("InventoryReportMapper Test Suite") +class InventoryReportMapperTest { + + private static final Date EXPIRY = Date.valueOf("2026-01-31"); + private static final Timestamp CREATED = Timestamp.valueOf("2025-01-31 10:15:30"); + + private final InventoryReportMapper mapper = InventoryReportMapper.INSTANCE; + + private static ItemStockEntryReport entryReport() { + ItemStockEntryReport report = new ItemStockEntryReport(); + report.setFacilityName("Main store"); + report.setItemName("Paracetamol"); + report.setItemCategoryName("Analgesic"); + report.setStrength("500mg"); + report.setBatchNo("B-1"); + report.setUnitCostPrice(2.5d); + report.setExpiryDate(EXPIRY); + report.setCreatedDate(CREATED); + report.setEntryType("Purchase"); + report.setQuantity(100); + report.setQuantityInHand(40); + return report; + } + + private static PatientIssueExitReport exitReport() { + PatientIssueExitReport report = new PatientIssueExitReport(); + report.setCreatedDate(CREATED); + report.setPatientName("Jane Doe"); + report.setGender("Female"); + report.setAge(34); + report.setItemName("Paracetamol"); + report.setItemCategoryName("Analgesic"); + report.setBatchNo("B-1"); + report.setExpiryDate(EXPIRY); + report.setStrength("500mg"); + report.setQuantityGiven(6); + return report; + } + + @Test + @DisplayName("mapInwardStockReport should carry the inward columns across, with the created date as inward date") + void mapInwardStockReport_shouldCarryInwardColumns() { + InwardStockReport report = mapper.mapInwardStockReport(entryReport()); + + assertEquals("Main store", report.getFacilityName()); + assertEquals("Paracetamol", report.getItemName()); + assertEquals("Analgesic", report.getItemCategory()); + assertEquals("B-1", report.getBatchNo()); + assertEquals(2.5d, report.getUnitCostPrice()); + assertEquals(EXPIRY, report.getExpiryDate()); + assertEquals(CREATED, report.getInwardDate()); + assertEquals("Purchase", report.getEntryType()); + assertEquals(100, report.getQuantity()); + } + + @Test + @DisplayName("mapInwardStockReport should return null for a null entry report") + void mapInwardStockReport_shouldReturnNullForNull() { + assertNull(mapper.mapInwardStockReport(null)); + } + + @Test + @DisplayName("mapExpiryReport should carry the expiry columns across, including the quantity still in hand") + void mapExpiryReport_shouldCarryExpiryColumns() { + ExpiryReport report = mapper.mapExpiryReport(entryReport()); + + assertEquals("Main store", report.getFacilityName()); + assertEquals("Paracetamol", report.getItemName()); + assertEquals("Analgesic", report.getItemCategory()); + assertEquals("500mg", report.getStrength()); + assertEquals("B-1", report.getBatchNo()); + assertEquals(2.5d, report.getUnitCostPrice()); + assertEquals(EXPIRY, report.getExpiryDate()); + assertEquals(40, report.getQuantityInHand()); + } + + @Test + @DisplayName("mapExpiryReport should return null for a null entry report") + void mapExpiryReport_shouldReturnNullForNull() { + assertNull(mapper.mapExpiryReport(null)); + } + + @Test + @DisplayName("mapBenDrugIssueReport should carry the beneficiary and the dispensed quantity across") + void mapBenDrugIssueReport_shouldCarryBeneficiaryAndQuantity() { + BenDrugIssueReport report = mapper.mapBenDrugIssueReport(exitReport()); + + assertEquals(CREATED, report.getDate()); + assertEquals("Jane Doe", report.getBeneficiaryName()); + assertEquals("Female", report.getGender()); + assertEquals(34, report.getAge()); + assertEquals("Paracetamol", report.getItemName()); + assertEquals("Analgesic", report.getItemCategory()); + assertEquals("B-1", report.getBatchNo()); + assertEquals(EXPIRY, report.getExpiryDate()); + assertEquals("500mg", report.getStrength()); + assertEquals(6, report.getDispensedQuantity()); + } + + @Test + @DisplayName("mapBenDrugIssueReport should return null for a null exit report") + void mapBenDrugIssueReport_shouldReturnNullForNull() { + assertNull(mapper.mapBenDrugIssueReport(null)); + } +} diff --git a/src/test/java/com/iemr/inventory/mapper/stockExit/StockExitMapperTest.java b/src/test/java/com/iemr/inventory/mapper/stockExit/StockExitMapperTest.java new file mode 100644 index 00000000..055ac08c --- /dev/null +++ b/src/test/java/com/iemr/inventory/mapper/stockExit/StockExitMapperTest.java @@ -0,0 +1,246 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.mapper.stockExit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Date; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.iemr.inventory.data.items.ItemMaster; +import com.iemr.inventory.data.stockExit.ItemStockExit; +import com.iemr.inventory.data.stockExit.ItemStockExitMap; +import com.iemr.inventory.data.stockentry.AllocateItemMap; +import com.iemr.inventory.data.stockentry.ItemBatchList; +import com.iemr.inventory.data.stockentry.ItemMasterWithQuantityMap; +import com.iemr.inventory.data.stockentry.ItemStockEntry; + +@DisplayName("Stock exit mapper Test Suite") +class StockExitMapperTest { + + private static final Date EXPIRY = Date.valueOf("2026-01-31"); + + private static ItemStockEntry stockEntry() { + ItemMaster item = new ItemMaster(); + item.setItemName("Paracetamol"); + item.setItemID(11); + + ItemStockEntry entry = new ItemStockEntry(); + entry.setItemStockEntryID(601); + entry.setFacilityID(7); + entry.setItemID(11); + entry.setBatchNo("B-1"); + entry.setExpiryDate(EXPIRY); + entry.setQuantity(100); + entry.setQuantityInHand(40); + entry.setCreatedBy("tester"); + entry.setDeleted(false); + entry.setItem(item); + return entry; + } + + private static ItemStockExit stockExit() { + ItemStockExit exit = new ItemStockExit(); + exit.setItemStockExitID(501L); + exit.setItemStockEntryID(601L); + exit.setFacilityID(7); + exit.setItemID(11); + exit.setQuantity(6); + exit.setDeleted(false); + exit.setCreatedBy("tester"); + exit.setItemStockEntry(stockEntry()); + return exit; + } + + @Nested + @DisplayName("ItemStockExitMapper") + class ItemStockExitMapperTests { + + private final ItemStockExitMapper mapper = ItemStockExitMapper.INSTANCE; + + @Test + @DisplayName("getItemStockExitMap should flatten the batch of the exit onto the projection") + void getItemStockExitMap_shouldFlattenBatch() { + ItemStockExitMap result = mapper.getItemStockExitMap(stockExit()); + + assertEquals("Paracetamol", result.getItemName()); + assertEquals("B-1", result.getBatchNo()); + assertEquals(EXPIRY, result.getExpiryDate()); + assertEquals(6, result.getQuantity()); + assertEquals("tester", result.getCreatedBy()); + assertEquals(Boolean.FALSE, result.getDeleted()); + } + + @Test + @DisplayName("getItemStockExitMap should return null for a null exit") + void getItemStockExitMap_shouldReturnNullForNull() { + assertNull(mapper.getItemStockExitMap((ItemStockExit) null)); + } + + @Test + @DisplayName("getItemStockExitMapList should project every exit in the list") + void getItemStockExitMapList_shouldProjectEveryExit() { + List result = mapper.getItemStockExitMapList(List.of(stockExit(), stockExit())); + + assertEquals(2, result.size()); + assertEquals("B-1", result.get(1).getBatchNo()); + } + + @Test + @DisplayName("getItemStockEntryMapList should project an entry row the same way") + void getItemStockEntryMapList_shouldProjectEntry() { + ItemStockExitMap result = mapper.getItemStockEntryMapList(stockEntry()); + + assertEquals("Paracetamol", result.getItemName()); + assertEquals("B-1", result.getBatchNo()); + assertEquals(100, result.getQuantity()); + } + + @Test + @DisplayName("getItemStockEntryMapList should project every entry in the list") + void getItemStockEntryMapList_shouldProjectEveryEntry() { + assertEquals(2, mapper.getItemStockEntryMapList(List.of(stockEntry(), stockEntry())).size()); + } + + @Test + @DisplayName("getItemStockEntryMapList should return null for a null list") + void getItemStockEntryMapList_shouldReturnNullForNullList() { + assertNull(mapper.getItemStockEntryMapList((List) null)); + } + } + + @Nested + @DisplayName("ItemBatchListMap") + class ItemBatchListMapTests { + + private final ItemBatchListMap mapper = ItemBatchListMap.INSTANCE; + + @Test + @DisplayName("getItemStockExitMap should carry the batch identity and both quantities across") + void getItemStockExitMap_shouldCarryBatchIdentity() { + ItemBatchList result = mapper.getItemStockExitMap(stockEntry()); + + assertEquals(601L, result.getItemStockEntryID()); + assertEquals(7, result.getFacilityID()); + assertEquals(11, result.getItemID()); + assertEquals(100, result.getQuantity()); + assertEquals(40, result.getQuantityInHand()); + assertEquals("B-1", result.getBatchNo()); + assertEquals(EXPIRY, result.getExpiryDate()); + } + + @Test + @DisplayName("getItemStockExitMap should return null for a null entry") + void getItemStockExitMap_shouldReturnNullForNull() { + assertNull(mapper.getItemStockExitMap(null)); + } + + @Test + @DisplayName("getItemStockExitMapList should project every batch in the list") + void getItemStockExitMapList_shouldProjectEveryBatch() { + assertEquals(2, mapper.getItemStockExitMapList(List.of(stockEntry(), stockEntry())).size()); + } + + @Test + @DisplayName("getItemStockExitMapList should return null for a null list") + void getItemStockExitMapList_shouldReturnNullForNullList() { + assertNull(mapper.getItemStockExitMapList(null)); + } + } + + @Nested + @DisplayName("ItemMasterWithQuantityMapper") + class ItemMasterWithQuantityMapperTests { + + private final ItemMasterWithQuantityMapper mapper = ItemMasterWithQuantityMapper.INSTANCE; + + @Test + @DisplayName("getItemStockExitMap should carry the batch identity and the item across") + void getItemStockExitMap_shouldCarryItem() { + ItemMasterWithQuantityMap result = mapper.getItemStockExitMap(stockEntry()); + + assertEquals(601L, result.getItemStockEntryID()); + assertEquals(7, result.getFacilityID()); + assertEquals(11, result.getItemID()); + assertEquals(40, result.getQuantityInHand()); + assertEquals("Paracetamol", result.getItem().getItemName()); + } + + @Test + @DisplayName("getItemStockExitMap should return null for a null entry") + void getItemStockExitMap_shouldReturnNullForNull() { + assertNull(mapper.getItemStockExitMap(null)); + } + + @Test + @DisplayName("getItemStockExitMapList should project every entry in the list") + void getItemStockExitMapList_shouldProjectEveryEntry() { + assertEquals(2, mapper.getItemStockExitMapList(List.of(stockEntry(), stockEntry())).size()); + } + + @Test + @DisplayName("getItemStockExitMapList should return null for a null list") + void getItemStockExitMapList_shouldReturnNullForNullList() { + assertNull(mapper.getItemStockExitMapList(null)); + } + } + + @Nested + @DisplayName("AllocateItemMapper") + class AllocateItemMapperTests { + + private final AllocateItemMapper mapper = AllocateItemMapper.INSTANCE; + + @Test + @DisplayName("getItemStockExitMap should carry the facility and item across") + void getItemStockExitMap_shouldCarryFacilityAndItem() { + AllocateItemMap result = mapper.getItemStockExitMap(stockExit()); + + assertEquals(7, result.getFacilityID()); + assertEquals(11, result.getItemID()); + } + + @Test + @DisplayName("getItemStockExitMap should return null for a null exit") + void getItemStockExitMap_shouldReturnNullForNull() { + assertNull(mapper.getItemStockExitMap(null)); + } + + @Test + @DisplayName("getItemStockExitMapList should project every exit in the list") + void getItemStockExitMapList_shouldProjectEveryExit() { + assertEquals(2, mapper.getItemStockExitMapList(List.of(stockExit(), stockExit())).size()); + } + + @Test + @DisplayName("getItemStockExitMapList should return null for a null list") + void getItemStockExitMapList_shouldReturnNullForNullList() { + assertNull(mapper.getItemStockExitMapList(null)); + } + } +} diff --git a/src/test/java/com/iemr/inventory/mapper/stockadjustment/StockAdjustmentItemDraftMapperTest.java b/src/test/java/com/iemr/inventory/mapper/stockadjustment/StockAdjustmentItemDraftMapperTest.java new file mode 100644 index 00000000..bd32bdb6 --- /dev/null +++ b/src/test/java/com/iemr/inventory/mapper/stockadjustment/StockAdjustmentItemDraftMapperTest.java @@ -0,0 +1,249 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.mapper.stockadjustment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.inventory.data.items.ItemMaster; +import com.iemr.inventory.data.stockadjustment.StockAdjustmentItem; +import com.iemr.inventory.data.stockadjustment.StockAdjustmentItemDraft; +import com.iemr.inventory.data.stockadjustment.StockAdjustmentItemDraftEdit; +import com.iemr.inventory.data.stockentry.ItemStockEntry; + +@DisplayName("StockAdjustmentItemDraftMapper Test Suite") +class StockAdjustmentItemDraftMapperTest { + + private final StockAdjustmentItemDraftMapper mapper = StockAdjustmentItemDraftMapper.INSTANCE; + + private static ItemStockEntry stockEntry(String batchNo, String itemName, Integer inHand) { + ItemStockEntry entry = new ItemStockEntry(); + entry.setBatchNo(batchNo); + entry.setQuantityInHand(inHand); + if (itemName != null) { + ItemMaster item = new ItemMaster(); + item.setItemName(itemName); + entry.setItem(item); + } + return entry; + } + + private static StockAdjustmentItemDraft draftItem(Long mapID, boolean deleted, ItemStockEntry entry) { + StockAdjustmentItemDraft draftItem = new StockAdjustmentItemDraft(); + draftItem.setSADraftItemMapID(mapID); + draftItem.setItemStockEntryID(601L); + draftItem.setAdjustedQuantity(4); + draftItem.setCreatedBy("tester"); + draftItem.setProviderServiceMapID(3); + draftItem.setIsAdded(true); + draftItem.setDeleted(deleted); + draftItem.setReason("damaged"); + draftItem.setItemStockEntry(entry); + return draftItem; + } + + private static StockAdjustmentItem adjustmentItem(Long mapID, ItemStockEntry entry) { + StockAdjustmentItem item = new StockAdjustmentItem(); + item.setSAItemMapID(mapID); + item.setItemStockEntryID(601L); + item.setAdjustedQuantity(4); + item.setCreatedBy("tester"); + item.setProviderServiceMapID(3); + item.setIsAdded(false); + item.setDeleted(false); + item.setReason("expired"); + item.setItemStockEntry(entry); + return item; + } + + @Test + @DisplayName("getStockAdjustmentItemDraftEdit should flatten the batch and item name onto the edit projection") + void getStockAdjustmentItemDraftEdit_shouldFlattenBatchAndItemName() { + StockAdjustmentItemDraftEdit edit = + mapper.getStockAdjustmentItemDraftEdit(draftItem(9L, false, stockEntry("B-1", "Paracetamol", 40))); + + assertEquals(9L, edit.getSADraftItemMapID()); + assertEquals(601L, edit.getItemStockEntryID()); + assertEquals("B-1", edit.getBatchID()); + assertEquals("Paracetamol", edit.getItemName()); + assertEquals(40, edit.getQuantityInHand()); + assertEquals(4, edit.getAdjustedQuantity()); + assertEquals("tester", edit.getCreatedBy()); + assertEquals(3, edit.getProviderServiceMapID()); + assertEquals(Boolean.TRUE, edit.getIsAdded()); + assertEquals("damaged", edit.getReason()); + } + + @Test + @DisplayName("getStockAdjustmentItemDraftEdit should leave the item name blank when the batch has no item") + void getStockAdjustmentItemDraftEdit_shouldLeaveItemNameBlankWithoutItem() { + StockAdjustmentItemDraftEdit edit = + mapper.getStockAdjustmentItemDraftEdit(draftItem(9L, false, stockEntry("B-1", null, 40))); + + assertEquals("B-1", edit.getBatchID()); + assertNull(edit.getItemName()); + } + + @Test + @DisplayName("getStockAdjustmentItemDraftEdit should leave the batch fields blank when no batch is attached") + void getStockAdjustmentItemDraftEdit_shouldLeaveBatchFieldsBlankWithoutBatch() { + StockAdjustmentItemDraftEdit edit = mapper.getStockAdjustmentItemDraftEdit(draftItem(9L, false, null)); + + assertNull(edit.getBatchID()); + assertNull(edit.getQuantityInHand()); + } + + @Test + @DisplayName("getStockAdjustmentItemDraftEdit should return an empty projection for a null draft item") + void getStockAdjustmentItemDraftEdit_shouldReturnEmptyProjectionForNull() { + StockAdjustmentItemDraftEdit edit = mapper.getStockAdjustmentItemDraftEdit(null); + + assertNull(edit.getSADraftItemMapID()); + } + + @Test + @DisplayName("getStockAdjustmentItemDraftEditList should leave the soft-deleted draft rows out") + void getStockAdjustmentItemDraftEditList_shouldSkipDeletedRows() { + List rows = new ArrayList<>(Arrays.asList( + draftItem(9L, false, stockEntry("B-1", "Paracetamol", 40)), + draftItem(10L, true, stockEntry("B-2", "Ibuprofen", 10)))); + + List result = mapper.getStockAdjustmentItemDraftEditList(rows); + + assertEquals(1, result.size()); + assertEquals(9L, result.get(0).getSADraftItemMapID()); + } + + @Test + @DisplayName("getStockAdjustmentItemDraftEditList should return nothing for an empty draft") + void getStockAdjustmentItemDraftEditList_shouldReturnNothingForEmptyDraft() { + assertTrue(mapper.getStockAdjustmentItemDraftEditList(new ArrayList<>()).isEmpty()); + } + + @Test + @DisplayName("getStockAdjustmentItemEditList should project every posted adjustment line") + void getStockAdjustmentItemEditList_shouldProjectEveryLine() { + List rows = new ArrayList<>(Arrays.asList( + adjustmentItem(21L, stockEntry("B-1", "Paracetamol", 40)), + adjustmentItem(22L, null))); + + List result = mapper.getStockAdjustmentItemEditList(rows); + + assertEquals(2, result.size()); + assertEquals(21L, result.get(0).getSAItemMapID()); + assertEquals("Paracetamol", result.get(0).getItemName()); + assertEquals("expired", result.get(0).getReason()); + assertNull(result.get(1).getBatchID()); + } + + @Test + @DisplayName("mapSADraftItemMapID should read any numeric-looking value as a long, and null as null") + void mapSADraftItemMapID_shouldReadNumericValues() { + assertEquals(9L, mapper.mapSADraftItemMapID("9")); + assertEquals(9L, mapper.mapSADraftItemMapID(9)); + assertNull(mapper.mapSADraftItemMapID(null)); + } + + @Test + @DisplayName("mapCreatedBy should stringify any value, and leave null as null") + void mapCreatedBy_shouldStringifyValues() { + assertEquals("tester", mapper.mapCreatedBy("tester")); + assertEquals("9", mapper.mapCreatedBy(9)); + assertNull(mapper.mapCreatedBy(null)); + } + + @Test + @DisplayName("mapCreatedDate should produce a date for any value, and null for null") + void mapCreatedDate_shouldProduceDateForAnyValue() { + assertTrue(mapper.mapCreatedDate("anything") != null); + assertNull(mapper.mapCreatedDate(null)); + } + + /** + * The MapStruct-generated delegate that sits behind the hand-written decorator. The decorator + * overrides every method, so these are the only tests that exercise the generated mapping code. + */ + @org.junit.jupiter.api.Nested + @DisplayName("Generated delegate") + class GeneratedDelegateTests { + + private final StockAdjustmentItemDraftMapper delegate = new StockAdjustmentItemDraftMapperImpl_(); + + @Test + @DisplayName("the delegate should copy the draft item's own fields onto the edit projection") + void delegate_shouldCopyDraftFields() { + StockAdjustmentItemDraftEdit edit = + delegate.getStockAdjustmentItemDraftEdit(draftItem(9L, false, stockEntry("B-1", "Paracetamol", 40))); + + assertEquals(9L, edit.getSADraftItemMapID()); + assertEquals(601L, edit.getItemStockEntryID()); + assertEquals(4, edit.getAdjustedQuantity()); + assertEquals("tester", edit.getCreatedBy()); + assertEquals(3, edit.getProviderServiceMapID()); + assertEquals("damaged", edit.getReason()); + } + + @Test + @DisplayName("the delegate should return null for a null draft item") + void delegate_shouldReturnNullForNullDraftItem() { + assertNull(delegate.getStockAdjustmentItemDraftEdit(null)); + } + + @Test + @DisplayName("the delegate should project every draft row, deleted ones included") + void delegate_shouldProjectEveryDraftRow() { + List rows = new ArrayList<>(Arrays.asList( + draftItem(9L, false, null), draftItem(10L, true, null))); + + assertEquals(2, delegate.getStockAdjustmentItemDraftEditList(rows).size()); + } + + @Test + @DisplayName("the delegate should return null for a null draft list") + void delegate_shouldReturnNullForNullDraftList() { + assertNull(delegate.getStockAdjustmentItemDraftEditList(null)); + } + + @Test + @DisplayName("the delegate should project every posted adjustment line") + void delegate_shouldProjectEveryAdjustmentLine() { + List rows = new ArrayList<>(Arrays.asList( + adjustmentItem(21L, null), adjustmentItem(22L, null))); + + assertEquals(2, delegate.getStockAdjustmentItemEditList(rows).size()); + } + + @Test + @DisplayName("the delegate should return null for a null adjustment list") + void delegate_shouldReturnNullForNullAdjustmentList() { + assertNull(delegate.getStockAdjustmentItemEditList(null)); + } + } +} diff --git a/src/test/java/com/iemr/inventory/service/dispenseagainst_rx/DispenseAgainstRXimplTest.java b/src/test/java/com/iemr/inventory/service/dispenseagainst_rx/DispenseAgainstRXimplTest.java new file mode 100644 index 00000000..b47ecbbb --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/dispenseagainst_rx/DispenseAgainstRXimplTest.java @@ -0,0 +1,205 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.dispenseagainst_rx; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +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 com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.iemr.inventory.repo.dispenseagainst_rx.PrescribedDrugDetailsRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("DispenseAgainstRXimpl Test Suite") +class DispenseAgainstRXimplTest { + + private static final String REQUEST = + "{\"beneficiaryRegID\":101,\"visitCode\":5001,\"facilityID\":7}"; + + @Mock + private PrescribedDrugDetailsRepo prescribedDrugDetailsRepo; + + @InjectMocks + private DispenseAgainstRXimpl service; + + /** + * Builds one result-set row in the column order the native query returns, with a batch that + * expires the given number of days from now and the given quantity in hand. + */ + private static Object[] row(Integer drugID, String batchNo, Integer stockEntryID, int qtyInHand, + int expiresInDays) { + Timestamp expiry = new Timestamp(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresInDays)); + return new Object[] { + 101L, 5001L, 9001L, drugID, + "Paracetamol", "Tablet", "500mg", "1", "Oral", "BD", + "5", "Days", "After food", "with water", + Timestamp.valueOf("2025-01-31 10:15:30"), "dr.smith", + stockEntryID, batchNo, qtyInHand, expiry, 10, Boolean.TRUE + }; + } + + @SuppressWarnings("unchecked") + private static Map parse(String json) { + return new Gson().fromJson(json, new TypeToken>() { + }.getType()); + } + + @SuppressWarnings("unchecked") + private static List> itemList(Map response) { + return (List>) response.get("itemList"); + } + + @SuppressWarnings("unchecked") + private static List> batchList(Map item) { + return (List>) item.get("batchList"); + } + + private void givenRows(Object[]... rows) { + when(prescribedDrugDetailsRepo.getPrescribedMedicinesWithDetails(101L, 5001L, 7)) + .thenReturn(new ArrayList<>(List.of(rows))); + } + + @Test + @DisplayName("getPrescribedMedicines should return null when the request cannot be read as a utility object") + void getPrescribedMedicines_shouldReturnNullForUnreadableRequest() { + assertNull(service.getPrescribedMedicines("null")); + } + + @Test + @DisplayName("getPrescribedMedicines should report the prescription header from the first row") + void getPrescribedMedicines_shouldReportPrescriptionHeader() { + givenRows(row(1, "B-1", 501, 20, 30)); + + Map response = parse(service.getPrescribedMedicines(REQUEST)); + + assertEquals(9001.0, response.get("prescriptionID")); + assertEquals(101.0, response.get("beneficiaryRegID")); + assertEquals(5001.0, response.get("visitCode")); + assertEquals("dr.smith", response.get("consultantName")); + } + + @Test + @DisplayName("getPrescribedMedicines should describe the prescribed drug alongside its dispensable batch") + void getPrescribedMedicines_shouldDescribeDrugAndBatch() { + givenRows(row(1, "B-1", 501, 20, 30)); + + Map item = itemList(parse(service.getPrescribedMedicines(REQUEST))).get(0); + + assertEquals(1.0, item.get("drugID")); + assertEquals("Paracetamol", item.get("genericDrugName")); + assertEquals("Tablet", item.get("drugForm")); + assertEquals("500mg", item.get("drugStrength")); + assertEquals("Oral", item.get("route")); + assertEquals("BD", item.get("frequency")); + assertEquals("Days", item.get("durationUnit")); + assertEquals("with water", item.get("specialInstruction")); + assertEquals(10.0, item.get("qtyPrescribed")); + assertEquals(Boolean.TRUE, item.get("isEDL")); + + Map batch = batchList(item).get(0); + assertEquals("B-1", batch.get("batchNo")); + assertEquals(501.0, batch.get("itemStockEntryID")); + assertEquals(20.0, batch.get("qty")); + } + + @Test + @DisplayName("getPrescribedMedicines should collect several batches of the same drug under one item") + void getPrescribedMedicines_shouldCollectSeveralBatchesUnderOneItem() { + givenRows(row(1, "B-1", 501, 20, 30), row(1, "B-2", 502, 5, 60)); + + List> items = itemList(parse(service.getPrescribedMedicines(REQUEST))); + + assertEquals(1, items.size()); + assertEquals(2, batchList(items.get(0)).size()); + } + + @Test + @DisplayName("getPrescribedMedicines should start a new item when the drug changes") + void getPrescribedMedicines_shouldStartNewItemWhenDrugChanges() { + givenRows(row(1, "B-1", 501, 20, 30), row(2, "B-9", 502, 8, 45)); + + List> items = itemList(parse(service.getPrescribedMedicines(REQUEST))); + + assertEquals(2, items.size()); + assertEquals(1.0, items.get(0).get("drugID")); + assertEquals(2.0, items.get(1).get("drugID")); + } + + @Test + @DisplayName("getPrescribedMedicines should leave an expired batch out of the dispensable list") + void getPrescribedMedicines_shouldExcludeExpiredBatch() { + givenRows(row(1, "B-old", 501, 20, -10)); + + Map item = itemList(parse(service.getPrescribedMedicines(REQUEST))).get(0); + + assertTrue(batchList(item).isEmpty()); + } + + @Test + @DisplayName("getPrescribedMedicines should leave an exhausted batch out of the dispensable list") + void getPrescribedMedicines_shouldExcludeExhaustedBatch() { + givenRows(row(1, "B-empty", 501, 0, 30)); + + Map item = itemList(parse(service.getPrescribedMedicines(REQUEST))).get(0); + + assertTrue(batchList(item).isEmpty()); + } + + @Test + @DisplayName("getPrescribedMedicines should return an empty payload when nothing was prescribed") + void getPrescribedMedicines_shouldReturnEmptyPayloadWhenNothingPrescribed() { + when(prescribedDrugDetailsRepo.getPrescribedMedicinesWithDetails(101L, 5001L, 7)) + .thenReturn(new ArrayList<>()); + + Map response = parse(service.getPrescribedMedicines(REQUEST)); + + assertTrue(response.isEmpty()); + } + + @Test + @DisplayName("getPrescribedMedicines should read a row that carries no stock entry through the short constructor") + void getPrescribedMedicines_shouldHandleRowWithoutStockEntry() { + Object[] row = row(1, "B-1", null, 20, 30); + givenRows(row); + + Map item = itemList(parse(service.getPrescribedMedicines(REQUEST))).get(0); + + assertEquals(1.0, item.get("drugID")); + assertFalse(item.containsKey("nonexistent")); + } +} diff --git a/src/test/java/com/iemr/inventory/service/drugtype/DrugtypeServiceImplTest.java b/src/test/java/com/iemr/inventory/service/drugtype/DrugtypeServiceImplTest.java new file mode 100644 index 00000000..e002494f --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/drugtype/DrugtypeServiceImplTest.java @@ -0,0 +1,121 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.drugtype; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +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 com.iemr.inventory.data.drugtype.M_Drugtype; +import com.iemr.inventory.repo.drugtype.DrugtypeRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("DrugtypeServiceImpl Test Suite") +class DrugtypeServiceImplTest { + + @Mock + private DrugtypeRepo drugtypeRepo; + + @InjectMocks + private DrugtypeServiceImpl service; + + private static M_Drugtype row(Integer id) { + M_Drugtype row = new M_Drugtype(); + row.setDrugTypeID(id); + return row; + } + + @Test + @DisplayName("createDrugtypeData should return the rows the repository saved") + void createDrugtypeData_shouldReturnSavedRows() { + List input = List.of(row(1), row(2)); + ArrayList saved = new ArrayList<>(input); + when(drugtypeRepo.saveAll(input)).thenReturn(saved); + + assertSame(saved, service.createDrugtypeData(input)); + } + + @Test + @DisplayName("createDrugtypeData should return null when the repository saved nothing") + void createDrugtypeData_shouldReturnNullWhenNothingSaved() { + List input = List.of(); + when(drugtypeRepo.saveAll(input)).thenReturn(new ArrayList()); + + assertNull(service.createDrugtypeData(input)); + } + + @Test + @DisplayName("getDrugtypeData should hand back the rows the repository found for the provider service map") + void getDrugtypeData_shouldReturnRowsForProviderServiceMap() { + ArrayList found = new ArrayList<>(List.of(row(1))); + when(drugtypeRepo.getDrugtypeData(3)).thenReturn(found); + + assertSame(found, service.getDrugtypeData(3)); + } + + @Test + @DisplayName("getDrugtypeData should return null when the provider service map has no rows") + void getDrugtypeData_shouldReturnNullWhenNoRows() { + when(drugtypeRepo.getDrugtypeData(3)).thenReturn(new ArrayList()); + + assertNull(service.getDrugtypeData(3)); + } + + @Test + @DisplayName("editDrugtypeData should hand back the row the repository looked up by id") + void editDrugtypeData_shouldReturnRowById() { + M_Drugtype found = row(1); + when(drugtypeRepo.geteditedData(1)).thenReturn(found); + + assertSame(found, service.editDrugtypeData(1)); + } + + @Test + @DisplayName("editDrugtypeData should hand back null when no row carries that id") + void editDrugtypeData_shouldReturnNullWhenRowMissing() { + when(drugtypeRepo.geteditedData(99)).thenReturn(null); + + assertNull(service.editDrugtypeData(99)); + } + + @Test + @DisplayName("saveeditDrugtype should persist the edited row and return what the repository stored") + void saveeditDrugtype_shouldPersistEditedRow() { + M_Drugtype edited = row(1); + when(drugtypeRepo.save(edited)).thenReturn(edited); + + assertEquals(edited, service.saveeditDrugtype(edited)); + verify(drugtypeRepo).save(edited); + } +} diff --git a/src/test/java/com/iemr/inventory/service/facilitytype/M_facilitytypeServiceImplTest.java b/src/test/java/com/iemr/inventory/service/facilitytype/M_facilitytypeServiceImplTest.java new file mode 100644 index 00000000..8f7bafcc --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/facilitytype/M_facilitytypeServiceImplTest.java @@ -0,0 +1,112 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.facilitytype; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +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 com.iemr.inventory.data.facilitytype.M_facilitytype; +import com.iemr.inventory.repository.facilitytype.M_facilitytypeRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("M_facilitytypeServiceImpl Test Suite") +class M_facilitytypeServiceImplTest { + + @Mock + private M_facilitytypeRepo m_facilitytypeRepo; + + @InjectMocks + private M_facilitytypeServiceImpl service; + + private static M_facilitytype row(Integer id) { + M_facilitytype row = new M_facilitytype(); + row.setFacilityTypeID(id); + return row; + } + + @Test + @DisplayName("getAllFicilityData should hand back the rows the repository found for the provider service map") + void getAllFicilityData_shouldReturnRowsForProviderServiceMap() { + ArrayList found = new ArrayList<>(List.of(row(1))); + when(m_facilitytypeRepo.getAllFicilityData(3)).thenReturn(found); + + assertSame(found, service.getAllFicilityData(3)); + } + + @Test + @DisplayName("getAllFicilityData should pass an empty result through untouched") + void getAllFicilityData_shouldPassEmptyResultThrough() { + when(m_facilitytypeRepo.getAllFicilityData(3)).thenReturn(new ArrayList()); + + assertTrue(service.getAllFicilityData(3).isEmpty()); + } + + @Test + @DisplayName("addAllFicilityData should return the rows the repository saved") + void addAllFicilityData_shouldReturnSavedRows() { + List input = List.of(row(1), row(2)); + ArrayList saved = new ArrayList<>(input); + when(m_facilitytypeRepo.saveAll(input)).thenReturn(saved); + + assertSame(saved, service.addAllFicilityData(input)); + } + + @Test + @DisplayName("editAllFicilityData should hand back the row the repository looked up by id") + void editAllFicilityData_shouldReturnRowById() { + M_facilitytype found = row(1); + when(m_facilitytypeRepo.findByFacilityTypeID(1)).thenReturn(found); + + assertSame(found, service.editAllFicilityData(1)); + } + + @Test + @DisplayName("editAllFicilityData should hand back null when no row carries that id") + void editAllFicilityData_shouldReturnNullWhenRowMissing() { + when(m_facilitytypeRepo.findByFacilityTypeID(99)).thenReturn(null); + + assertNull(service.editAllFicilityData(99)); + } + + @Test + @DisplayName("updateFacilityData should persist the edited row and return what the repository stored") + void updateFacilityData_shouldPersistEditedRow() { + M_facilitytype edited = row(1); + when(m_facilitytypeRepo.save(edited)).thenReturn(edited); + + assertSame(edited, service.updateFacilityData(edited)); + verify(m_facilitytypeRepo).save(edited); + } +} diff --git a/src/test/java/com/iemr/inventory/service/health/HealthServiceTest.java b/src/test/java/com/iemr/inventory/service/health/HealthServiceTest.java new file mode 100644 index 00000000..e8367c25 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/health/HealthServiceTest.java @@ -0,0 +1,345 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.health; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.util.ReflectionTestUtils; + +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.HikariPoolMXBean; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("HealthService Test Suite") +class HealthServiceTest { + + @Mock + private DataSource dataSource; + @Mock + private RedisTemplate redisTemplate; + @Mock + private Connection connection; + @Mock + private PreparedStatement statement; + @Mock + private ResultSet resultSet; + + private HealthService healthService; + + @BeforeEach + @DisplayName("Wire a service over a healthy MySQL and Redis by default") + void setUp() throws Exception { + when(dataSource.getConnection()).thenReturn(connection); + when(connection.prepareStatement(anyString())).thenReturn(statement); + when(statement.executeQuery()).thenReturn(resultSet); + when(resultSet.next()).thenReturn(true); + when(resultSet.getInt(1)).thenReturn(0); + when(redisTemplate.execute(any(RedisCallback.class))).thenReturn("PONG"); + + healthService = new HealthService(dataSource, redisTemplate); + } + + @SuppressWarnings("unchecked") + private Map component(Map response, String name) { + Map> components = + (Map>) response.get("components"); + return components.get(name); + } + + @Nested + @DisplayName("Overall status aggregation") + class OverallStatusTests { + + @Test + @DisplayName("checkHealth should report UP when MySQL and Redis are both healthy") + void checkHealth_shouldReportUpWhenAllComponentsHealthy() { + Map response = healthService.checkHealth(); + + assertEquals("UP", response.get("status")); + assertNotNull(response.get("timestamp")); + assertEquals("UP", component(response, "mysql").get("status")); + assertEquals("UP", component(response, "redis").get("status")); + assertEquals("OK", component(response, "mysql").get("severity")); + } + + @Test + @DisplayName("checkHealth should report DOWN when MySQL cannot be reached") + void checkHealth_shouldReportDownWhenMysqlUnreachable() throws Exception { + when(dataSource.getConnection()).thenThrow(new SQLException("connection refused")); + + Map response = healthService.checkHealth(); + + assertEquals("DOWN", response.get("status")); + assertEquals("DOWN", component(response, "mysql").get("status")); + assertEquals("CRITICAL", component(response, "mysql").get("severity")); + assertEquals("MySQL connection failed", component(response, "mysql").get("error")); + } + + @Test + @DisplayName("checkHealth should report DOWN when the MySQL probe returns no row") + void checkHealth_shouldReportDownWhenProbeReturnsNoRow() throws Exception { + when(resultSet.next()).thenReturn(false); + + Map response = healthService.checkHealth(); + + assertEquals("DOWN", response.get("status")); + assertEquals("No result from health check query", component(response, "mysql").get("error")); + } + + @Test + @DisplayName("checkHealth should always expose a response time for each component") + void checkHealth_shouldExposeResponseTimePerComponent() { + Map response = healthService.checkHealth(); + + assertNotNull(component(response, "mysql").get("responseTimeMs")); + assertNotNull(component(response, "redis").get("responseTimeMs")); + } + } + + @Nested + @DisplayName("Redis health") + class RedisHealthTests { + + @Test + @DisplayName("checkHealth should treat an unconfigured Redis as healthy and say so") + void checkHealth_shouldSkipRedisWhenNotConfigured() { + HealthService serviceWithoutRedis = new HealthService(dataSource, null); + + Map response = serviceWithoutRedis.checkHealth(); + + assertEquals("UP", response.get("status")); + assertEquals("UP", component(response, "redis").get("status")); + assertEquals("Redis not configured — skipped", component(response, "redis").get("message")); + } + + @Test + @DisplayName("checkHealth should report DOWN when Redis answers something other than PONG") + void checkHealth_shouldReportDownWhenRedisPingFails() { + when(redisTemplate.execute(any(RedisCallback.class))).thenReturn("NOPE"); + + Map response = healthService.checkHealth(); + + assertEquals("DOWN", response.get("status")); + assertEquals("Redis PING failed", component(response, "redis").get("error")); + } + + @Test + @DisplayName("checkHealth should report DOWN when the Redis call throws") + void checkHealth_shouldReportDownWhenRedisThrows() { + when(redisTemplate.execute(any(RedisCallback.class))) + .thenThrow(new IllegalStateException("redis unavailable")); + + Map response = healthService.checkHealth(); + + assertEquals("DOWN", response.get("status")); + assertEquals("Redis connection failed", component(response, "redis").get("error")); + } + } + + @Nested + @DisplayName("Advanced MySQL diagnostics") + class AdvancedDiagnosticsTests { + + @Test + @DisplayName("checkHealth should flag DEGRADED when MySQL reports lock waits") + void checkHealth_shouldFlagDegradedOnLockWaits() throws Exception { + when(resultSet.getInt(1)).thenReturn(4); + + Map response = healthService.checkHealth(); + + assertEquals("DEGRADED", response.get("status")); + assertEquals("DEGRADED", component(response, "mysql").get("status")); + assertEquals("WARNING", component(response, "mysql").get("severity")); + } + + @Test + @DisplayName("checkHealth should stay UP when the diagnostic counters are all clear") + void checkHealth_shouldStayUpWhenDiagnosticsClear() throws Exception { + when(resultSet.getInt(1)).thenReturn(0); + + Map response = healthService.checkHealth(); + + assertEquals("UP", response.get("status")); + } + + @Test + @DisplayName("checkHealth should stay UP when the advanced diagnostics cannot get a connection") + void checkHealth_shouldStayUpWhenAdvancedConnectionUnavailable() throws Exception { + when(dataSource.getConnection()).thenReturn(connection).thenThrow(new SQLException("pool exhausted")); + + Map response = healthService.checkHealth(); + + assertEquals("UP", response.get("status")); + } + + @Test + @DisplayName("checkHealth should throttle the advanced diagnostics to one run per window") + void checkHealth_shouldThrottleAdvancedDiagnostics() throws Exception { + // The first check takes two connections: one for the basic probe, one for the diagnostics. + healthService.checkHealth(); + verify(dataSource, times(2)).getConnection(); + + // The second check reuses the cached diagnostic result, so only the basic probe connects. + healthService.checkHealth(); + verify(dataSource, times(3)).getConnection(); + } + + @Test + @DisplayName("checkHealth should flag DEGRADED when the HikariCP pool is nearly exhausted") + void checkHealth_shouldFlagDegradedWhenHikariPoolNearlyExhausted() throws Exception { + HikariDataSource hikariDataSource = mock(HikariDataSource.class); + HikariPoolMXBean poolMXBean = mock(HikariPoolMXBean.class); + when(hikariDataSource.getConnection()).thenReturn(connection); + when(hikariDataSource.getHikariPoolMXBean()).thenReturn(poolMXBean); + when(hikariDataSource.getMaximumPoolSize()).thenReturn(10); + when(poolMXBean.getActiveConnections()).thenReturn(9); + + HealthService service = new HealthService(hikariDataSource, redisTemplate); + Map response = service.checkHealth(); + + assertEquals("DEGRADED", response.get("status")); + } + + @Test + @DisplayName("checkHealth should stay UP when the HikariCP pool still has headroom") + void checkHealth_shouldStayUpWhenHikariPoolHasHeadroom() throws Exception { + HikariDataSource hikariDataSource = mock(HikariDataSource.class); + HikariPoolMXBean poolMXBean = mock(HikariPoolMXBean.class); + when(hikariDataSource.getConnection()).thenReturn(connection); + when(hikariDataSource.getHikariPoolMXBean()).thenReturn(poolMXBean); + when(hikariDataSource.getMaximumPoolSize()).thenReturn(10); + when(poolMXBean.getActiveConnections()).thenReturn(2); + + HealthService service = new HealthService(hikariDataSource, redisTemplate); + + assertEquals("UP", service.checkHealth().get("status")); + } + + @Test + @DisplayName("checkHealth should stay UP when the HikariCP MX bean is unavailable") + void checkHealth_shouldStayUpWhenHikariMxBeanUnavailable() throws Exception { + HikariDataSource hikariDataSource = mock(HikariDataSource.class); + when(hikariDataSource.getConnection()).thenReturn(connection); + when(hikariDataSource.getHikariPoolMXBean()).thenReturn(null); + + HealthService service = new HealthService(hikariDataSource, redisTemplate); + + assertEquals("UP", service.checkHealth().get("status")); + } + + @Test + @DisplayName("checkHealth should stay UP when the diagnostic queries themselves fail") + void checkHealth_shouldStayUpWhenDiagnosticQueriesFail() throws Exception { + when(connection.prepareStatement(anyString())) + .thenReturn(statement) + .thenThrow(new SQLException("information_schema unavailable")); + + Map response = healthService.checkHealth(); + + assertEquals("UP", response.get("status")); + } + } + + @Nested + @DisplayName("Executor lifecycle") + class ShutdownTests { + + @Test + @DisplayName("shutdown should stop the executor so no further checks are submitted") + void shutdown_shouldStopTheExecutor() throws Exception { + healthService.shutdown(); + + Map response = healthService.checkHealth(); + + assertEquals("DOWN", response.get("status")); + assertEquals("MySQL health check did not complete in time", component(response, "mysql").get("error")); + assertEquals("Redis health check did not complete in time", component(response, "redis").get("error")); + verify(dataSource, never()).getConnection(); + } + + @Test + @DisplayName("shutdown should be safe to call more than once") + void shutdown_shouldBeIdempotent() { + healthService.shutdown(); + + healthService.shutdown(); + + assertTrue(true, "a repeated shutdown must not throw"); + } + } + + @Test + @DisplayName("checkHealth should set the MySQL query timeout so a hung DB cannot stall the probe") + void checkHealth_shouldSetQueryTimeout() throws Exception { + healthService.checkHealth(); + + verify(statement, atLeastOnce()).setQueryTimeout(anyInt()); + } + + @Test + @DisplayName("checkHealth should close the JDBC resources it opens") + void checkHealth_shouldCloseJdbcResources() throws Exception { + healthService.checkHealth(); + + verify(connection, atLeastOnce()).close(); + verify(statement, atLeastOnce()).close(); + } + + @Test + @DisplayName("checkHealth should not report a false DEGRADED after a clean run") + void checkHealth_shouldNotReportFalseDegraded() { + assertFalse("DEGRADED".equals(healthService.checkHealth().get("status"))); + verify(redisTemplate, times(1)).execute(any(RedisCallback.class)); + } +} diff --git a/src/test/java/com/iemr/inventory/service/indent/IndentServiceImplTest.java b/src/test/java/com/iemr/inventory/service/indent/IndentServiceImplTest.java new file mode 100644 index 00000000..baee6405 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/indent/IndentServiceImplTest.java @@ -0,0 +1,402 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.indent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.sql.Date; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import com.iemr.inventory.data.indent.Indent; +import com.iemr.inventory.data.indent.IndentIssue; +import com.iemr.inventory.data.indent.IndentOrder; +import com.iemr.inventory.data.indent.ItemfacilitymappingIndent; +import com.iemr.inventory.data.stockExit.ItemStockExit; +import com.iemr.inventory.data.stockentry.ItemStockEntry; +import com.iemr.inventory.repo.indent.IndentIssueRepo; +import com.iemr.inventory.repo.indent.IndentOrderRepo; +import com.iemr.inventory.repo.indent.IndentRepo; +import com.iemr.inventory.repo.indent.ItemfacilitymappingIndentRepo; +import com.iemr.inventory.repo.stockEntry.ItemStockEntryRepo; +import com.iemr.inventory.repo.stockExit.ItemStockExitRepo; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("IndentServiceImpl Test Suite") +class IndentServiceImplTest { + + @Mock + private ItemfacilitymappingIndentRepo itemfacilitymappingIndentRepo; + @Mock + private IndentOrderRepo indentOrderRepo; + @Mock + private IndentRepo indentRepo; + @Mock + private IndentIssueRepo indentIssueRepo; + @Mock + private ItemStockExitRepo itemStockExitRepo; + @Mock + private ItemStockEntryRepo itemStockEntryRepo; + + @Mock(answer = org.mockito.Answers.RETURNS_DEEP_STUBS) + private jakarta.persistence.EntityManager entityManager; + + @InjectMocks + private IndentServiceImpl service; + + private static ArrayList rows(Object[]... values) { + ArrayList list = new ArrayList<>(); + for (Object[] value : values) { + list.add(value); + } + return list; + } + + private static Indent indent(Long indentID) { + Indent indent = new Indent(); + indent.setIndentID(indentID); + indent.setFromFacilityID(1); + indent.setToFacilityID(2); + indent.setVanID(4L); + indent.setParkingPlaceID(2L); + indent.setProviderServiceMapID(3); + indent.setCreatedBy("tester"); + indent.setVanSerialNo(55L); + indent.setSyncFacilityID(1); + indent.setIndentOrder(new ArrayList<>()); + return indent; + } + + private static IndentOrder order(Long orderID) { + IndentOrder order = new IndentOrder(); + order.setIndentOrderID(orderID); + order.setItemID(11L); + order.setRequiredQty(20L); + return order; + } + + private static IndentIssue issue(String action, Integer issuedQty) { + IndentIssue issue = new IndentIssue(); + issue.setIndentID(88L); + issue.setIndentIssueID(9L); + issue.setItemID(11); + issue.setIssuedQty(issuedQty); + issue.setItemStockEntryID(601L); + issue.setFromFacilityID(1); + issue.setToFacilityID(2); + issue.setCreatedBy("tester"); + issue.setVanID(4L); + issue.setParkingPlaceID(2L); + issue.setUnitCostPrice(2.5d); + issue.setBatchNo("B-1"); + issue.setExpiryDate(Date.valueOf("2026-01-31")); + issue.setAction(action); + issue.setRejectedReason("out of stock"); + return issue; + } + + @Test + @DisplayName("findItemIndent should build an indentable item out of each result row") + void findItemIndent_shouldBuildItemPerRow() { + when(itemfacilitymappingIndentRepo.findindentitem(1, "Para")).thenReturn(rows(new Object[] { + 11, "ITM-11", "Paracetamol", Boolean.TRUE, "500mg", "Tablet", "Analgesic", "Tablet", + "NSAID", "paracetamol", 1, BigDecimal.valueOf(40) })); + + List result = service.findItemIndent(1, "Para"); + + assertEquals(1, result.size()); + ItemfacilitymappingIndent item = result.get(0); + assertEquals(11, item.getItemID()); + assertEquals("ITM-11", item.getItemCode()); + assertEquals("Paracetamol", item.getItemName()); + assertEquals(Boolean.TRUE, item.getIsMedical()); + assertEquals("500mg", item.getStrength()); + assertEquals("Tablet", item.getUomName()); + assertEquals("Analgesic", item.getItemCategory()); + assertEquals("NSAID", item.getPharmacologicalCategoryName()); + assertEquals("paracetamol", item.getComposition()); + assertEquals(1, item.getFacilityID()); + assertEquals(BigDecimal.valueOf(40), item.getQoh()); + } + + @Test + @DisplayName("findItemIndent should report a zero quantity on hand when the row carries none") + void findItemIndent_shouldReportZeroQuantityWhenAbsent() { + when(itemfacilitymappingIndentRepo.findindentitem(1, "Para")).thenReturn(rows(new Object[] { + 11, "ITM-11", "Paracetamol", Boolean.TRUE, "500mg", "Tablet", "Analgesic", "Tablet", + "NSAID", "paracetamol", 1, null })); + + assertEquals(BigDecimal.ZERO, service.findItemIndent(1, "Para").get(0).getQoh()); + } + + @Test + @DisplayName("createIndentRequest should open the indent as pending and stamp its order lines") + void createIndentRequest_shouldOpenPendingIndent() { + Indent request = indent(null); + request.getIndentOrder().add(order(null)); + Indent persisted = indent(88L); + when(indentRepo.save(request)).thenReturn(persisted); + when(indentOrderRepo.saveAll(anyList())).thenAnswer(inv -> inv.getArgument(0)); + + service.createIndentRequest(request); + + assertEquals(1, request.getSyncFacilityID()); + assertEquals("Pending", request.getStatus()); + assertEquals("N", request.getProcessed()); + IndentOrder stamped = request.getIndentOrder().get(0); + assertEquals(88L, stamped.getIndentID()); + assertEquals(4L, stamped.getVanID()); + assertEquals(3, stamped.getProviderServiceMapID()); + assertEquals("tester", stamped.getCreatedBy()); + assertEquals("Pending", stamped.getStatus()); + assertEquals(1, stamped.getFromFacilityID()); + verify(indentRepo).updateVanSerialNo(88L, 1); + verify(indentOrderRepo).updateVanSerialNo(); + } + + @Test + @DisplayName("getIndentHistory should list the indents the requesting facility raised") + void getIndentHistory_shouldListIndentsOfFacility() { + when(indentOrderRepo.getIndentHistory(1)).thenReturn(List.of(indent(88L))); + + assertTrue(service.getIndentHistory(indent(88L)).contains("indentID")); + } + + @Test + @DisplayName("getOrdersByIndentID should resolve the indent first, then read its order lines by sync keys") + void getOrdersByIndentID_shouldResolveIndentThenLines() { + IndentOrder probe = order(null); + probe.setIndentID(88L); + when(indentRepo.findByIndentID(88L)).thenReturn(indent(88L)); + when(indentOrderRepo.getOrdersByIndentID(55L, 1)).thenReturn(List.of(order(9L))); + + assertTrue(service.getOrdersByIndentID(probe).contains("indentOrderID")); + verify(indentOrderRepo).getOrdersByIndentID(55L, 1); + } + + @Test + @DisplayName("getIndentOrderWorklist should read the order lines of the resolved indent") + void getIndentOrderWorklist_shouldReadOrderLines() { + IndentOrder probe = order(null); + probe.setIndentID(88L); + when(indentRepo.findByIndentID(88L)).thenReturn(indent(88L)); + when(indentOrderRepo.getOrdersByIndentID(55L, 1)).thenReturn(List.of(order(9L))); + + assertTrue(service.getIndentOrderWorklist(probe).contains("indentOrderID")); + } + + /** Points the deep-stubbed EntityManager at a canned result list for the criteria query. */ + private jakarta.persistence.criteria.CriteriaBuilder givenCriteriaQueryReturnsOneIndent() { + jakarta.persistence.criteria.CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + jakarta.persistence.criteria.CriteriaQuery query = builder.createQuery(Indent.class); + when(entityManager.createQuery(query).getResultList()).thenReturn(List.of(indent(88L))); + return builder; + } + + @Test + @DisplayName("getIndentWorklist should ask only for pending indents raised under the given main facility") + void getIndentWorklist_shouldAskForPendingIndentsOfMainFacility() { + IndentOrder probe = order(null); + probe.setFacilityID(2); + jakarta.persistence.criteria.CriteriaBuilder builder = givenCriteriaQueryReturnsOneIndent(); + + assertTrue(service.getIndentWorklist(probe).contains("indentID")); + + verify(builder).equal(any(), eq("Pending")); + verify(builder).equal(any(), eq(2)); + verify(builder, never()).between(any(jakarta.persistence.criteria.Expression.class), + any(java.sql.Timestamp.class), any(java.sql.Timestamp.class)); + verify(builder, never()).isNotNull(any()); + } + + @Test + @DisplayName("getIndentWorklist should narrow to a date window when both ends are supplied") + void getIndentWorklist_shouldNarrowToDateWindow() { + IndentOrder probe = order(null); + probe.setFacilityID(2); + probe.setStartDateTime(java.sql.Timestamp.valueOf("2025-01-01 00:00:00")); + probe.setEndDateTime(java.sql.Timestamp.valueOf("2025-01-31 23:59:00")); + jakarta.persistence.criteria.CriteriaBuilder builder = givenCriteriaQueryReturnsOneIndent(); + + service.getIndentWorklist(probe); + + verify(builder).between(any(jakarta.persistence.criteria.Expression.class), + eq(probe.getStartDateTime()), eq(probe.getEndDateTime())); + } + + @Test + @DisplayName("getIndentWorklist should narrow to one requesting facility when one is supplied") + void getIndentWorklist_shouldNarrowToRequestingFacility() { + IndentOrder probe = order(null); + probe.setFacilityID(2); + probe.setIndentFromID(1); + jakarta.persistence.criteria.CriteriaBuilder builder = givenCriteriaQueryReturnsOneIndent(); + + service.getIndentWorklist(probe); + + verify(builder).isNotNull(any()); + verify(builder).equal(any(), eq(1)); + } + + @Test + @DisplayName("issueIndent should book the stock out and record the issue for an issued line") + void issueIndent_shouldBookStockOutForIssuedLine() { + when(indentRepo.findByIndentID(88L)).thenReturn(indent(88L)); + + assertEquals("Dispensed successfully", service.issueIndent(new IndentIssue[] { issue("Issued", 6) })); + + verify(indentOrderRepo).issueIndent("Issued", 55L, 1, "out of stock"); + verify(indentOrderRepo).issueIndentOrder("Issued", 55L, 1); + verify(indentOrderRepo).updateQuantityInStock(6, 601L, 1); + verify(indentIssueRepo).save(any(IndentIssue.class)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ItemStockExit.class); + verify(itemStockExitRepo).save(captor.capture()); + ItemStockExit exit = captor.getValue(); + assertEquals(601L, exit.getItemStockEntryID()); + assertEquals(1, exit.getSyncFacilityID()); + assertEquals(6, exit.getQuantity()); + assertEquals(88L, exit.getExitTypeID()); + assertEquals("t_indent", exit.getExitType()); + assertEquals("tester", exit.getCreatedBy()); + verify(indentIssueRepo).updateVanSerialNo(); + verify(itemStockExitRepo).updateVanSerialNo(); + } + + @Test + @DisplayName("issueIndent should record a rejection without touching any stock") + void issueIndent_shouldRecordRejectionWithoutTouchingStock() { + when(indentRepo.findByIndentID(88L)).thenReturn(indent(88L)); + + assertEquals("Rejected successfully", service.issueIndent(new IndentIssue[] { issue("Rejected", 0) })); + + verify(indentOrderRepo, never()).updateQuantityInStock(anyInt(), anyLong(), anyInt()); + verify(itemStockExitRepo, never()).save(any(ItemStockExit.class)); + } + + @Test + @DisplayName("cancelIndentOrder should cancel both the indent and its order lines") + void cancelIndentOrder_shouldCancelIndentAndLines() { + Indent probe = indent(88L); + when(indentRepo.findByIndentID(88L)).thenReturn(indent(88L)); + + assertEquals("Cancelled successfully", service.cancelIndentOrder(probe)); + + verify(indentOrderRepo).cancelIndent(88L); + verify(indentOrderRepo).cancelIndentOrder(55L, 1); + } + + @Test + @DisplayName("receiveIndent should book the issued quantities into the receiving store") + void receiveIndent_shouldBookIssuedQuantitiesIntoReceivingStore() { + Indent request = indent(88L); + when(indentRepo.findByIndentID(88L)).thenReturn(indent(88L)); + when(indentOrderRepo.getIndentIssued(55L, 2)).thenReturn(List.of(issue("Issued", 6))); + + assertEquals("Received successfully", service.receiveIndent(request)); + + verify(indentOrderRepo).acceptIndent(88L, 1); + verify(indentOrderRepo).acceptIndentOrder(55L, 1); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(itemStockEntryRepo).saveAll(captor.capture()); + ItemStockEntry booked = captor.getValue().get(0); + assertEquals(1, booked.getFacilityID()); + assertEquals(11, booked.getItemID()); + assertEquals(6, booked.getQuantity()); + assertEquals(6, booked.getQuantityInHand()); + assertEquals(2.5d, booked.getTotalCostPrice()); + assertEquals("B-1", booked.getBatchNo()); + assertEquals(88L, booked.getEntryTypeID()); + assertEquals("Indent", booked.getEntryType()); + assertEquals("tester", booked.getCreatedBy()); + verify(itemStockEntryRepo).updateItemStockEntryVanSerialNo(); + } + + @Test + @DisplayName("receiveIndent should book nothing for a line that was issued in zero quantity") + void receiveIndent_shouldBookNothingForZeroQuantity() { + Indent request = indent(88L); + when(indentRepo.findByIndentID(88L)).thenReturn(indent(88L)); + when(indentOrderRepo.getIndentIssued(55L, 2)).thenReturn(List.of(issue("Issued", 0))); + + service.receiveIndent(request); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(itemStockEntryRepo).saveAll(captor.capture()); + assertTrue(captor.getValue().isEmpty()); + } + + @Test + @DisplayName("updateIndentOrder should stamp a newly added order line with the indent's details") + void updateIndentOrder_shouldStampNewOrderLine() { + Indent request = indent(88L); + request.getIndentOrder().add(order(null)); + when(indentRepo.save(request)).thenReturn(indent(88L)); + when(indentOrderRepo.saveAll(anyList())).thenAnswer(inv -> inv.getArgument(0)); + + assertEquals("Updated successfully", service.updateIndentOrder(request)); + + IndentOrder stamped = request.getIndentOrder().get(0); + assertEquals(88L, stamped.getIndentID()); + assertEquals("Pending", stamped.getStatus()); + assertEquals("N", stamped.getProcessed()); + assertEquals(1, stamped.getSyncFacilityID()); + } + + @Test + @DisplayName("updateIndentOrder should only refresh the sync facility on an order line that already exists") + void updateIndentOrder_shouldOnlyRefreshSyncFacilityOnExistingLine() { + Indent request = indent(88L); + request.getIndentOrder().add(order(9L)); + when(indentRepo.save(request)).thenReturn(indent(88L)); + when(indentOrderRepo.saveAll(anyList())).thenAnswer(inv -> inv.getArgument(0)); + + service.updateIndentOrder(request); + + IndentOrder existing = request.getIndentOrder().get(0); + assertEquals(1, existing.getSyncFacilityID()); + assertTrue(existing.getStatus() == null, "an existing line keeps whatever status it already carried"); + } +} diff --git a/src/test/java/com/iemr/inventory/service/item/ItemServiceImplTest.java b/src/test/java/com/iemr/inventory/service/item/ItemServiceImplTest.java new file mode 100644 index 00000000..b8486b42 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/item/ItemServiceImplTest.java @@ -0,0 +1,284 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.item; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +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 com.iemr.inventory.data.items.ItemMaster; +import com.iemr.inventory.data.items.M_ItemCategory; +import com.iemr.inventory.data.items.M_ItemForm; +import com.iemr.inventory.data.items.M_Route; +import com.iemr.inventory.repository.item.ItemCategoryRepo; +import com.iemr.inventory.repository.item.ItemFormRepo; +import com.iemr.inventory.repository.item.ItemRepo; +import com.iemr.inventory.repository.item.RouteRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ItemServiceImpl Test Suite") +class ItemServiceImplTest { + + @Mock + private ItemRepo itemRepo; + @Mock + private ItemCategoryRepo itemCategoryRepo; + @Mock + private RouteRepo routeRepo; + @Mock + private ItemFormRepo itemFormRepo; + + @InjectMocks + private ItemServiceImpl service; + + private static ItemMaster item(Integer id) { + ItemMaster item = new ItemMaster(); + item.setItemID(id); + return item; + } + + private static M_ItemCategory category(Integer id, String issueType) { + M_ItemCategory category = new M_ItemCategory(); + category.setItemCategoryID(id); + category.setIssueType(issueType); + return category; + } + + @Test + @DisplayName("createItemMaster should persist through the item repository") + void createItemMaster_shouldPersist() { + ItemMaster item = item(1); + when(itemRepo.save(item)).thenReturn(item); + + assertSame(item, service.createItemMaster(item)); + } + + @Test + @DisplayName("getItemCategory(all) should return every category for the provider service map") + void getItemCategory_shouldReturnAllCategories() { + List all = List.of(category(1, "Bulk")); + when(itemCategoryRepo.findByProviderServiceMapID(3)).thenReturn(all); + + assertSame(all, service.getItemCategory(true, 3)); + verify(itemCategoryRepo, never()).findByDeletedAndProviderServiceMapID(false, 3); + } + + @Test + @DisplayName("getItemCategory(not all) should exclude the deleted categories") + void getItemCategory_shouldExcludeDeletedCategories() { + List live = List.of(category(1, "Bulk")); + when(itemCategoryRepo.findByDeletedAndProviderServiceMapID(false, 3)).thenReturn(live); + + assertSame(live, service.getItemCategory(false, 3)); + } + + @Test + @DisplayName("getItemCategory should return an empty list rather than query with a null provider service map") + void getItemCategory_shouldReturnEmptyWhenProviderServiceMapNull() { + assertTrue(service.getItemCategory(true, null).isEmpty()); + verify(itemCategoryRepo, never()).findByProviderServiceMapID(anyInt()); + } + + @Test + @DisplayName("getItemRoute(all) should return every route") + void getItemRoute_shouldReturnAllRoutes() { + List all = List.of(new M_Route()); + when(routeRepo.getAll()).thenReturn(all); + + assertSame(all, service.getItemRoute(true)); + } + + @Test + @DisplayName("getItemRoute(not all) should exclude the deleted routes") + void getItemRoute_shouldExcludeDeletedRoutes() { + List live = List.of(new M_Route()); + when(routeRepo.findByDeleted(false)).thenReturn(live); + + assertSame(live, service.getItemRoute(false)); + } + + @Test + @DisplayName("getItemForm(all) should return every item form") + void getItemForm_shouldReturnAllForms() { + List all = List.of(new M_ItemForm()); + when(itemFormRepo.getAll()).thenReturn(all); + + assertSame(all, service.getItemForm(true)); + } + + @Test + @DisplayName("getItemForm(not all) should exclude the deleted item forms") + void getItemForm_shouldExcludeDeletedForms() { + List live = List.of(new M_ItemForm()); + when(itemFormRepo.findByDeleted(false)).thenReturn(live); + + assertSame(live, service.getItemForm(false)); + } + + @Test + @DisplayName("getItemMaster should return the items of the provider service map") + void getItemMaster_shouldReturnItemsOfProviderServiceMap() { + List items = List.of(item(1)); + when(itemRepo.findByProviderServiceMapID(3)).thenReturn(items); + + assertSame(items, service.getItemMaster(3)); + } + + @Test + @DisplayName("blockItemMaster should pass the delete flag straight to the repository") + void blockItemMaster_shouldPassDeleteFlagThrough() { + when(itemRepo.deleteItemMaster(9, true)).thenReturn(1); + + assertEquals(1, service.blockItemMaster(9, true)); + } + + @Test + @DisplayName("discontinueItemMaster should pass the discontinue flag straight to the repository") + void discontinueItemMaster_shouldPassFlagThrough() { + when(itemRepo.discontinueItemMaster(9, false)).thenReturn(1); + + assertEquals(1, service.discontinueItemMaster(9, false)); + } + + @Test + @DisplayName("addAllItemMaster should save the whole batch in one call") + void addAllItemMaster_shouldSaveBatch() { + List batch = List.of(item(1), item(2)); + when(itemRepo.saveAll(batch)).thenReturn(batch); + + assertSame(batch, service.addAllItemMaster(batch)); + } + + @Test + @DisplayName("getItemMasterByID should unwrap the item the repository found") + void getItemMasterByID_shouldUnwrapFoundItem() { + ItemMaster item = item(1); + when(itemRepo.findById(1)).thenReturn(Optional.of(item)); + + assertSame(item, service.getItemMasterByID(1)); + } + + @Test + @DisplayName("getItemMasterByID should throw when no item carries that id") + void getItemMasterByID_shouldThrowWhenItemMissing() { + when(itemRepo.findById(99)).thenReturn(Optional.empty()); + + assertThrows(NoSuchElementException.class, () -> service.getItemMasterByID(99)); + } + + @Test + @DisplayName("getItemMasterCatByID should return the detailed row the repository built") + void getItemMasterCatByID_shouldReturnDetailRow() { + ItemMaster item = item(1); + when(itemRepo.findDetailOne(1)).thenReturn(item); + + assertSame(item, service.getItemMasterCatByID(1)); + } + + @Test + @DisplayName("updateItemIssueConfig should total the rows updated for every complete category") + void updateItemIssueConfig_shouldTotalUpdatedRows() { + when(itemCategoryRepo.updateIssueConfig(1, "Bulk")).thenReturn(1); + when(itemCategoryRepo.updateIssueConfig(2, "Single")).thenReturn(1); + + assertEquals(2, service.updateItemIssueConfig(List.of(category(1, "Bulk"), category(2, "Single")))); + } + + @Test + @DisplayName("updateItemIssueConfig should skip categories missing an id or an issue type") + void updateItemIssueConfig_shouldSkipIncompleteCategories() { + assertEquals(0, service.updateItemIssueConfig(List.of(category(null, "Bulk"), category(2, null)))); + verify(itemCategoryRepo, never()).updateIssueConfig(anyInt(), org.mockito.ArgumentMatchers.anyString()); + } + + @Test + @DisplayName("getItemRouteProviderServiceMapID should delegate to the route repository") + void getItemRouteProviderServiceMapID_shouldDelegate() { + List routes = List.of(new M_Route()); + when(routeRepo.findByProviderServiceMapID(3)).thenReturn(routes); + + assertSame(routes, service.getItemRouteProviderServiceMapID(3)); + } + + @Test + @DisplayName("getItemFormProviderServiceMapID should delegate to the item form repository") + void getItemFormProviderServiceMapID_shouldDelegate() { + List forms = List.of(new M_ItemForm()); + when(itemFormRepo.findByProviderServiceMapID(3)).thenReturn(forms); + + assertSame(forms, service.getItemFormProviderServiceMapID(3)); + } + + @Test + @DisplayName("getItemMasters should narrow the items to one category of one provider service map") + void getItemMasters_shouldNarrowByCategory() { + List items = List.of(item(1)); + when(itemRepo.getItemMasters(3, 5)).thenReturn(items); + + assertSame(items, service.getItemMasters(3, 5)); + } + + @Test + @DisplayName("getItemCategory(id) should unwrap the category the repository found") + void getItemCategoryById_shouldUnwrapFoundCategory() { + M_ItemCategory category = category(5, "Bulk"); + when(itemCategoryRepo.findById(5)).thenReturn(Optional.of(category)); + + assertSame(category, service.getItemCategory(5)); + } + + @Test + @DisplayName("getItemCategory(id) should throw when no category carries that id") + void getItemCategoryById_shouldThrowWhenCategoryMissing() { + when(itemCategoryRepo.findById(99)).thenReturn(Optional.empty()); + + assertThrows(NoSuchElementException.class, () -> service.getItemCategory(99)); + } + + @Test + @DisplayName("getActiveItemMaster should filter on the deleted flag carried by the probe item") + void getActiveItemMaster_shouldFilterOnDeletedFlag() { + ItemMaster probe = item(null); + probe.setDeleted(false); + probe.setProviderServiceMapID(3); + List active = List.of(item(1)); + when(itemRepo.findByDeletedAndProviderServiceMapID(false, 3)).thenReturn(active); + + assertSame(active, service.getActiveItemMaster(probe)); + } +} diff --git a/src/test/java/com/iemr/inventory/service/itemfacilitymapping/M_itemfacilitymappingImplTest.java b/src/test/java/com/iemr/inventory/service/itemfacilitymapping/M_itemfacilitymappingImplTest.java new file mode 100644 index 00000000..ad7272fd --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/itemfacilitymapping/M_itemfacilitymappingImplTest.java @@ -0,0 +1,227 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.itemfacilitymapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +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.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.iemr.inventory.data.items.ItemInStore; +import com.iemr.inventory.data.items.ItemMaster; +import com.iemr.inventory.data.itemfacilitymapping.M_itemfacilitymapping; +import com.iemr.inventory.data.itemfacilitymapping.V_fetchItemFacilityMap; +import com.iemr.inventory.data.stockentry.ItemStockEntry; +import com.iemr.inventory.repo.stockEntry.ItemStockEntryRepo; +import com.iemr.inventory.repository.item.ItemRepo; +import com.iemr.inventory.repository.itemfacilitymapping.M_itemfacilitymappingRepo; +import com.iemr.inventory.repository.itemfacilitymapping.V_fetchItemFacilityMapRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("M_itemfacilitymappingImpl Test Suite") +class M_itemfacilitymappingImplTest { + + @Mock + private M_itemfacilitymappingRepo m_itemfacilitymappingRepo; + @Mock + private V_fetchItemFacilityMapRepo v_fetchItemFacilityMapRepo; + @Mock + private ItemStockEntryRepo itemStockEntryRepo; + @Mock + private ItemRepo itemRepo; + + @InjectMocks + private M_itemfacilitymappingImpl service; + + /** Wraps native-query rows, which are Object arrays that List.of() would otherwise flatten. */ + private static ArrayList rows(Object[]... values) { + ArrayList list = new ArrayList<>(); + for (Object[] value : values) { + list.add(value); + } + return list; + } + + @Test + @DisplayName("mapItemtoStore should save the whole mapping batch in one call") + void mapItemtoStore_shouldSaveBatch() { + List batch = List.of(new M_itemfacilitymapping()); + ArrayList saved = new ArrayList<>(batch); + when(m_itemfacilitymappingRepo.saveAll(batch)).thenReturn(saved); + + assertSame(saved, service.mapItemtoStore(batch)); + } + + @Test + @DisplayName("editdata should unwrap the mapping the repository found") + void editdata_shouldUnwrapFoundMapping() { + M_itemfacilitymapping mapping = new M_itemfacilitymapping(); + when(m_itemfacilitymappingRepo.findById(5)).thenReturn(Optional.of(mapping)); + + assertSame(mapping, service.editdata(5)); + } + + @Test + @DisplayName("editdata should throw when no mapping carries that id") + void editdata_shouldThrowWhenMappingMissing() { + when(m_itemfacilitymappingRepo.findById(99)).thenReturn(Optional.empty()); + + assertThrows(NoSuchElementException.class, () -> service.editdata(99)); + } + + @Test + @DisplayName("saveEditedItem should persist the edited mapping") + void saveEditedItem_shouldPersistEditedMapping() { + M_itemfacilitymapping mapping = new M_itemfacilitymapping(); + when(m_itemfacilitymappingRepo.save(mapping)).thenReturn(mapping); + + assertSame(mapping, service.saveEditedItem(mapping)); + } + + @Test + @DisplayName("getsubitemforsubStote should build a mapping out of each result-set row") + void getsubitemforsubStote_shouldBuildMappingPerRow() { + when(m_itemfacilitymappingRepo.getItemforSubstore(3, 7)).thenReturn(rows(new Object[] { 11, "Paracetamol", Boolean.FALSE, 2 }, + new Object[] { 12, "Ibuprofen", Boolean.TRUE, 3 })); + + ArrayList result = service.getsubitemforsubStote(3, 7); + + assertEquals(2, result.size()); + assertEquals(11, result.get(0).getItemID()); + assertEquals("Paracetamol", result.get(0).getItemName()); + assertEquals(Boolean.TRUE, result.get(1).getDiscontinued()); + assertEquals(3, result.get(1).getItemCategoryID()); + } + + @Test + @DisplayName("getsubitemforsubStote should skip a short or absent result-set row") + void getsubitemforsubStote_shouldSkipShortRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { 11, "Paracetamol" }); + when(m_itemfacilitymappingRepo.getItemforSubstore(3, 7)).thenReturn(rows); + + assertTrue(service.getsubitemforsubStote(3, 7).isEmpty()); + } + + @Test + @DisplayName("getAllFacilityMappedData should delegate to the mapped-data view repository") + void getAllFacilityMappedData_shouldDelegate() { + ArrayList mapped = new ArrayList<>(List.of(new V_fetchItemFacilityMap())); + when(v_fetchItemFacilityMapRepo.getAllFacilityMappedData(3)).thenReturn(mapped); + + assertSame(mapped, service.getAllFacilityMappedData(3)); + } + + @Test + @DisplayName("getItemMastersFromStoreID should look the quantities up for every item mapped to the store") + void getItemMastersFromStoreID_shouldReturnQuantitiesPerItem() { + when(m_itemfacilitymappingRepo.getItemforStore(7)).thenReturn(rows(new Object[] { 11, "Paracetamol" }, new Object[] { 12, "Ibuprofen" })); + when(itemStockEntryRepo.getQuantity(any(Integer[].class), eq(7))).thenReturn(rows(new Object[] { 7, 11, "Paracetamol", 40L })); + + List result = service.getItemMastersFromStoreID(7); + + assertEquals(1, result.size()); + assertEquals(7, result.get(0).getFacilityID()); + assertEquals(11, result.get(0).getItemID()); + assertEquals("Paracetamol", result.get(0).getItemName()); + assertEquals(40L, result.get(0).getQuantity()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Integer[].class); + verify(itemStockEntryRepo).getQuantity(captor.capture(), eq(7)); + assertEquals(2, captor.getValue().length); + } + + @Test + @DisplayName("getItemMastersFromStoreID should return nothing when the store has no mapped items") + void getItemMastersFromStoreID_shouldReturnNothingWhenStoreEmpty() { + when(m_itemfacilitymappingRepo.getItemforStore(7)).thenReturn(new ArrayList<>()); + + assertTrue(service.getItemMastersFromStoreID(7).isEmpty()); + verify(itemStockEntryRepo, never()).getQuantity(any(Integer[].class), anyInt()); + } + + @Test + @DisplayName("getItemMastersPartialSearch should resolve the matched ids into full item rows") + void getItemMastersPartialSearch_shouldResolveMatchedIds() { + when(m_itemfacilitymappingRepo.getItemforStorePartialSearch(7, "Para")) + .thenReturn(rows(new Object[] { 11, "Paracetamol" })); + List items = List.of(new ItemMaster()); + when(itemRepo.findByItemIDIn(any(Integer[].class))).thenReturn(items); + + assertSame(items, service.getItemMastersPartialSearch("Para", 7)); + } + + @Test + @DisplayName("getItemMastersPartialSearch should return nothing when the search matches no item") + void getItemMastersPartialSearch_shouldReturnNothingWhenNoMatch() { + when(m_itemfacilitymappingRepo.getItemforStorePartialSearch(7, "zzz")).thenReturn(new ArrayList<>()); + + assertTrue(service.getItemMastersPartialSearch("zzz", 7).isEmpty()); + verify(itemRepo, never()).findByItemIDIn(any(Integer[].class)); + } + + @Test + @DisplayName("getItemBatchForStoreTransfer should keep only unexpired batches the receiving store also stocks") + void getItemBatchForStoreTransfer_shouldNarrowToBatchesBothStoresStock() { + when(m_itemfacilitymappingRepo.getItemforStoreLikeItemName(1, "Para")) + .thenReturn(rows(new Object[] { 11, "Paracetamol" })); + when(m_itemfacilitymappingRepo.getItemforStoreAndItemID(eq(2), any(Integer[].class))) + .thenReturn(rows(new Object[] { 11, "Paracetamol" })); + List batches = List.of(new ItemStockEntry()); + when(itemStockEntryRepo.findByFacilityIDAndItemIDInAndQuantityInHandGreaterThanAndExpiryDateAfter( + eq(1), any(Integer[].class), eq(0), any(Date.class))).thenReturn(batches); + + assertSame(batches, service.getItemBatchForStoreTransfer(1, 2, "Para")); + } + + @Test + @DisplayName("getItemBatchForStoreTransfer should return nothing when the sending store stocks no such item") + void getItemBatchForStoreTransfer_shouldReturnNothingWhenSendingStoreHasNoMatch() { + when(m_itemfacilitymappingRepo.getItemforStoreLikeItemName(1, "zzz")).thenReturn(new ArrayList<>()); + + assertTrue(service.getItemBatchForStoreTransfer(1, 2, "zzz").isEmpty()); + verify(itemStockEntryRepo, never()) + .findByFacilityIDAndItemIDInAndQuantityInHandGreaterThanAndExpiryDateAfter( + anyInt(), any(Integer[].class), anyInt(), any(Date.class)); + } +} diff --git a/src/test/java/com/iemr/inventory/service/manufacturer/ManufacturerServiceImplTest.java b/src/test/java/com/iemr/inventory/service/manufacturer/ManufacturerServiceImplTest.java new file mode 100644 index 00000000..85cb5e4b --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/manufacturer/ManufacturerServiceImplTest.java @@ -0,0 +1,121 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.manufacturer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +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 com.iemr.inventory.data.manufacturer.M_Manufacturer; +import com.iemr.inventory.repo.manufacturer.ManufacturerRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ManufacturerServiceImpl Test Suite") +class ManufacturerServiceImplTest { + + @Mock + private ManufacturerRepo manufacturerRepo; + + @InjectMocks + private ManufacturerServiceImpl service; + + private static M_Manufacturer row(Integer id) { + M_Manufacturer row = new M_Manufacturer(); + row.setManufacturerID(id); + return row; + } + + @Test + @DisplayName("createManufacturer should return the rows the repository saved") + void createManufacturer_shouldReturnSavedRows() { + List input = List.of(row(1), row(2)); + ArrayList saved = new ArrayList<>(input); + when(manufacturerRepo.saveAll(input)).thenReturn(saved); + + assertSame(saved, service.createManufacturer(input)); + } + + @Test + @DisplayName("createManufacturer should return null when the repository saved nothing") + void createManufacturer_shouldReturnNullWhenNothingSaved() { + List input = List.of(); + when(manufacturerRepo.saveAll(input)).thenReturn(new ArrayList()); + + assertNull(service.createManufacturer(input)); + } + + @Test + @DisplayName("createManufacturer should hand back the rows the repository found for the provider service map") + void createManufacturer_shouldReturnRowsForProviderServiceMap() { + ArrayList found = new ArrayList<>(List.of(row(1))); + when(manufacturerRepo.getManufacturerData(3)).thenReturn(found); + + assertSame(found, service.createManufacturer(3)); + } + + @Test + @DisplayName("createManufacturer should return null when the provider service map has no rows") + void createManufacturer_shouldReturnNullWhenNoRows() { + when(manufacturerRepo.getManufacturerData(3)).thenReturn(new ArrayList()); + + assertNull(service.createManufacturer(3)); + } + + @Test + @DisplayName("editManufacturer should hand back the row the repository looked up by id") + void editManufacturer_shouldReturnRowById() { + M_Manufacturer found = row(1); + when(manufacturerRepo.getEditData(1)).thenReturn(found); + + assertSame(found, service.editManufacturer(1)); + } + + @Test + @DisplayName("editManufacturer should hand back null when no row carries that id") + void editManufacturer_shouldReturnNullWhenRowMissing() { + when(manufacturerRepo.getEditData(99)).thenReturn(null); + + assertNull(service.editManufacturer(99)); + } + + @Test + @DisplayName("saveEditedData should persist the edited row and return what the repository stored") + void saveEditedData_shouldPersistEditedRow() { + M_Manufacturer edited = row(1); + when(manufacturerRepo.save(edited)).thenReturn(edited); + + assertEquals(edited, service.saveEditedData(edited)); + verify(manufacturerRepo).save(edited); + } +} diff --git a/src/test/java/com/iemr/inventory/service/patientreturn/PatientReturnServiceImplTest.java b/src/test/java/com/iemr/inventory/service/patientreturn/PatientReturnServiceImplTest.java new file mode 100644 index 00000000..963a3365 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/patientreturn/PatientReturnServiceImplTest.java @@ -0,0 +1,246 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.patientreturn; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.iemr.inventory.data.patientreturn.ItemDetailModel; +import com.iemr.inventory.data.patientreturn.PatientReturnModel; +import com.iemr.inventory.data.patientreturn.ReturnHistoryModel; +import com.iemr.inventory.data.stockExit.ItemReturnEntry; +import com.iemr.inventory.data.stockExit.T_PatientIssue; +import com.iemr.inventory.repository.patientreturn.ItemReturnEntryRepo; +import com.iemr.inventory.repository.patientreturn.PatientReturnRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("PatientReturnServiceImpl Test Suite") +class PatientReturnServiceImplTest { + + private static final Timestamp ISSUE_DATE = Timestamp.valueOf("2025-01-31 10:15:30"); + + @Mock + private PatientReturnRepo patientReturnRepo; + @Mock + private ItemReturnEntryRepo itemReturnEntryRepo; + + @InjectMocks + private PatientReturnServiceImpl service; + + /** ItemDetailModel has no no-arg constructor, so probes go through the all-args one. */ + private static ItemDetailModel probe(Long benRegID, Integer itemID, Integer facilityID) { + return new ItemDetailModel(itemID, null, null, null, null, null, null, null, null, null, null, + benRegID, null, facilityID); + } + + /** Wraps native-query rows, which are Object arrays that List.of() would otherwise flatten. */ + private static List rows(Object[]... values) { + List list = new ArrayList<>(); + for (Object[] value : values) { + list.add(value); + } + return list; + } + + @Test + @DisplayName("getItemNameByRegID should build one item row per result, over a ninety-day look-back") + void getItemNameByRegID_shouldBuildOneRowPerResult() { + T_PatientIssue issue = new T_PatientIssue(); + issue.setBenRegID(101L); + issue.setFacilityID(7); + when(patientReturnRepo.getItemNameByRegID(eqLong(101L), eqInt(7), any(Timestamp.class))) + .thenReturn(rows(new Object[] { 101L, 7, 11, "Paracetamol" }, + new Object[] { 101L, 7, 12, "Ibuprofen" })); + + List result = service.getItemNameByRegID(issue); + + assertEquals(2, result.size()); + assertEquals(101L, result.get(0).getBenRegID()); + assertEquals(7, result.get(0).getFacilityID()); + assertEquals(11, result.get(0).getItemID()); + assertEquals("Ibuprofen", result.get(1).getItemName()); + } + + private static Long eqLong(long value) { + return org.mockito.ArgumentMatchers.eq(value); + } + + private static Integer eqInt(int value) { + return org.mockito.ArgumentMatchers.eq(value); + } + + @Test + @DisplayName("getItemNameByRegID should skip an absent or empty result row") + void getItemNameByRegID_shouldSkipEmptyRow() { + T_PatientIssue issue = new T_PatientIssue(); + issue.setBenRegID(101L); + issue.setFacilityID(7); + List results = rows(new Object[0]); + results.add(null); + when(patientReturnRepo.getItemNameByRegID(anyLong(), anyInt(), any(Timestamp.class))).thenReturn(results); + + assertTrue(service.getItemNameByRegID(issue).isEmpty()); + } + + @Test + @DisplayName("getItemDetailByBen should build the full issue detail out of each result row") + void getItemDetailByBen_shouldBuildFullIssueDetail() { + ItemDetailModel probe = probe(101L, 11, 7); + when(patientReturnRepo.getItemDetailByBen(101L, 11, 7)).thenReturn(rows(new Object[] { + 11, "Paracetamol", "B-1", 20, ISSUE_DATE, Boolean.FALSE, Boolean.FALSE, + 501L, 601L, 701L, 801L, 101L, 3, 7 })); + + List result = service.getItemDetailByBen(probe); + + assertEquals(1, result.size()); + ItemDetailModel detail = result.get(0); + assertEquals(11, detail.getItemID()); + assertEquals("Paracetamol", detail.getItemName()); + assertEquals("B-1", detail.getBatchNo()); + assertEquals(20, detail.getIssuedQuantity()); + assertEquals(ISSUE_DATE, detail.getDateofIssue()); + assertEquals(Boolean.FALSE, detail.getDiscontinued()); + assertEquals(501L, detail.getItemStockExitID()); + assertEquals(601L, detail.getItemStockEntryID()); + assertEquals(101L, detail.getBenRegID()); + assertEquals(3, detail.getProviderServiceMapID()); + assertEquals(7, detail.getFacilityID()); + } + + @Test + @DisplayName("getItemDetailByBen should skip an absent or empty result row") + void getItemDetailByBen_shouldSkipEmptyRow() { + ItemDetailModel probe = probe(null, null, null); + List results = rows(new Object[0]); + results.add(null); + when(patientReturnRepo.getItemDetailByBen(null, null, null)).thenReturn(results); + + assertTrue(service.getItemDetailByBen(probe).isEmpty()); + } + + @Test + @DisplayName("updateQuantityReturned should restock and un-issue each returned line, then log the return") + void updateQuantityReturned_shouldRestockAndLogReturn() { + ItemDetailModel returned = probe(101L, 11, 7); + returned.setReturnQuantity(4); + returned.setItemStockEntryID(601L); + returned.setItemStockExitID(501L); + returned.setProviderServiceMapID(3); + returned.setVisitID(701L); + returned.setVisitCode(801L); + returned.setCreatedBy("tester"); + + String result = service.updateQuantityReturned(new ItemDetailModel[] { returned }); + + assertEquals("Quantity updated successfully", result); + verify(patientReturnRepo).updateQuantityReturned(4, 601L); + verify(patientReturnRepo).updateIssuedQuantity(4, 501L); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(itemReturnEntryRepo).saveAll(captor.capture()); + ItemReturnEntry entry = captor.getValue().get(0); + assertEquals(101L, entry.getBenRegID()); + assertEquals(501L, entry.getItemStockExitID()); + assertEquals(7, entry.getFacilityID()); + assertEquals(3, entry.getProviderServiceMapID()); + assertEquals(701L, entry.getVisitID()); + assertEquals(801L, entry.getVisitCode()); + assertEquals("tester", entry.getCreatedBy()); + } + + @Test + @DisplayName("updateQuantityReturned should still record an empty batch without touching any stock") + void updateQuantityReturned_shouldRecordEmptyBatch() { + assertEquals("Quantity updated successfully", service.updateQuantityReturned(new ItemDetailModel[0])); + + verify(itemReturnEntryRepo).saveAll(new ArrayList()); + } + + @Test + @DisplayName("getBenReturnHistory should build a history row for each return in the window") + void getBenReturnHistory_shouldBuildHistoryRows() { + ItemReturnEntry probe = new ItemReturnEntry(); + probe.setFacilityID(7); + probe.setFromDate(Timestamp.valueOf("2025-01-01 00:00:00")); + probe.setToDate(Timestamp.valueOf("2025-01-31 23:59:00")); + when(patientReturnRepo.getBenReturnHistory(7, probe.getFromDate(), probe.getToDate())) + .thenReturn(rows(new Object[] { "Paracetamol", "B-1", 20, ISSUE_DATE, 701L, 801L, + "Jane Doe", 34, "Female", ISSUE_DATE })); + + List result = service.getBenReturnHistory(probe); + + assertEquals(1, result.size()); + ReturnHistoryModel history = result.get(0); + assertEquals("Paracetamol", history.getItemName()); + assertEquals("B-1", history.getBatchNo()); + assertEquals(20, history.getIssuedQuantity()); + assertEquals(701L, history.getVisitID()); + assertEquals(801L, history.getVisitCode()); + assertEquals("Jane Doe", history.getPatientName()); + assertEquals(34, history.getAge()); + assertEquals("Female", history.getGender()); + } + + @Test + @DisplayName("getBenReturnHistory should leave the visit ids null when the return carries none") + void getBenReturnHistory_shouldLeaveVisitIdsNullWhenAbsent() { + ItemReturnEntry probe = new ItemReturnEntry(); + probe.setFacilityID(7); + when(patientReturnRepo.getBenReturnHistory(7, null, null)) + .thenReturn(rows(new Object[] { "Paracetamol", "B-1", 20, ISSUE_DATE, null, null, + "Jane Doe", 34, "Female", ISSUE_DATE })); + + ReturnHistoryModel history = service.getBenReturnHistory(probe).get(0); + + assertNull(history.getVisitID()); + assertNull(history.getVisitCode()); + } + + @Test + @DisplayName("getBenReturnHistory should skip an absent or empty result row") + void getBenReturnHistory_shouldSkipEmptyRow() { + ItemReturnEntry probe = new ItemReturnEntry(); + List results = rows(new Object[0]); + results.add(null); + when(patientReturnRepo.getBenReturnHistory(null, null, null)).thenReturn(results); + + assertTrue(service.getBenReturnHistory(probe).isEmpty()); + } +} diff --git a/src/test/java/com/iemr/inventory/service/pharmacologicalcategory/PharmacologicalcategoryServiceImplTest.java b/src/test/java/com/iemr/inventory/service/pharmacologicalcategory/PharmacologicalcategoryServiceImplTest.java new file mode 100644 index 00000000..d2a554b7 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/pharmacologicalcategory/PharmacologicalcategoryServiceImplTest.java @@ -0,0 +1,122 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.pharmacologicalcategory; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +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 com.iemr.inventory.data.pharmacologicalcategory.M_Pharmacologicalcategory; +import com.iemr.inventory.repo.pharmacologicalcategory.PharmacologicalcategoryRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("PharmacologicalcategoryServiceImpl Test Suite") +class PharmacologicalcategoryServiceImplTest { + + @Mock + private PharmacologicalcategoryRepo pharmacologicalcategoryRepo; + + @InjectMocks + private PharmacologicalcategoryServiceImpl service; + + private static M_Pharmacologicalcategory row(Integer id) { + M_Pharmacologicalcategory row = new M_Pharmacologicalcategory(); + row.setPharmCategoryID(id); + return row; + } + + @Test + @DisplayName("createPharmacologicalcategory should return the rows the repository saved") + void createPharmacologicalcategory_shouldReturnSavedRows() { + List input = List.of(row(1), row(2)); + ArrayList saved = new ArrayList<>(input); + when(pharmacologicalcategoryRepo.saveAll(input)).thenReturn(saved); + + assertSame(saved, service.createPharmacologicalcategory(input)); + } + + @Test + @DisplayName("createPharmacologicalcategory should return null when the repository saved nothing") + void createPharmacologicalcategory_shouldReturnNullWhenNothingSaved() { + List input = List.of(); + when(pharmacologicalcategoryRepo.saveAll(input)).thenReturn(new ArrayList()); + + assertNull(service.createPharmacologicalcategory(input)); + } + + @Test + @DisplayName("getPharmacologicalcategory should hand back whatever the repository found, including an empty list") + void getPharmacologicalcategory_shouldPassRepositoryResultThrough() { + ArrayList found = new ArrayList<>(List.of(row(1))); + when(pharmacologicalcategoryRepo.getPhormacologicalData(3)).thenReturn(found); + + assertSame(found, service.getPharmacologicalcategory(3)); + } + + @Test + @DisplayName("getPharmacologicalcategory should return the empty list rather than null when nothing matches") + void getPharmacologicalcategory_shouldReturnEmptyListWhenNoRows() { + when(pharmacologicalcategoryRepo.getPhormacologicalData(3)) + .thenReturn(new ArrayList()); + + assertTrue(service.getPharmacologicalcategory(3).isEmpty()); + } + + @Test + @DisplayName("editPharmacologicalcategory should hand back the row the repository looked up by id") + void editPharmacologicalcategory_shouldReturnRowById() { + M_Pharmacologicalcategory found = row(1); + when(pharmacologicalcategoryRepo.editPhamacologicalData(1)).thenReturn(found); + + assertSame(found, service.editPharmacologicalcategory(1)); + } + + @Test + @DisplayName("editPharmacologicalcategory should hand back null when no row carries that id") + void editPharmacologicalcategory_shouldReturnNullWhenRowMissing() { + when(pharmacologicalcategoryRepo.editPhamacologicalData(99)).thenReturn(null); + + assertNull(service.editPharmacologicalcategory(99)); + } + + @Test + @DisplayName("saveEditedPharData should persist the edited row and return what the repository stored") + void saveEditedPharData_shouldPersistEditedRow() { + M_Pharmacologicalcategory edited = row(1); + when(pharmacologicalcategoryRepo.save(edited)).thenReturn(edited); + + assertSame(edited, service.saveEditedPharData(edited)); + verify(pharmacologicalcategoryRepo).save(edited); + } +} diff --git a/src/test/java/com/iemr/inventory/service/report/CRMReportServiceImplTest.java b/src/test/java/com/iemr/inventory/service/report/CRMReportServiceImplTest.java new file mode 100644 index 00000000..4b04343b --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/report/CRMReportServiceImplTest.java @@ -0,0 +1,567 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.report; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Date; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import com.iemr.inventory.data.report.ItemStockEntryReport; +import com.iemr.inventory.data.report.ItemStockExitReport; +import com.iemr.inventory.data.report.PatientIssueExitReport; +import com.iemr.inventory.mapper.report.InventoryReportMapper; +import com.iemr.inventory.model.report.BenDrugIssueReport; +import com.iemr.inventory.model.report.InwardStockReport; +import com.iemr.inventory.repo.report.ItemStockReportRepo; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("CRMReportServiceImpl Test Suite") +class CRMReportServiceImplTest { + + private static final Timestamp START = Timestamp.valueOf("2025-01-01 00:00:00"); + private static final Timestamp END = Timestamp.valueOf("2025-01-31 23:59:00"); + private static final Date EXPIRY = Date.valueOf("2026-01-31"); + + @Mock + private ItemStockReportRepo itemStockReportRepo; + @Mock + private InventoryReportMapper mapper; + + @InjectMocks + private CRMReportServiceImpl service; + + private static ArrayList rows(Object[]... values) { + ArrayList list = new ArrayList<>(); + for (Object[] value : values) { + list.add(value); + } + return list; + } + + private static ItemStockEntryReport request(Integer facilityID) { + ItemStockEntryReport request = new ItemStockEntryReport(); + request.setFacilityID(facilityID); + request.setStartDate(START); + request.setEndDate(END); + return request; + } + + /** Builds a row in the 19-column shape the daily stock detail query returns. */ + private static Object[] stockDetailRow() { + Object[] row = new Object[19]; + row[3] = "B-1"; + row[4] = 100; + row[5] = 2.5d; + row[6] = EXPIRY; + row[9] = Timestamp.valueOf("2025-01-05 09:00:00"); + row[10] = 40; + row[11] = 6; + row[12] = "Paracetamol"; + row[13] = "Main store"; + row[14] = "Analgesic"; + row[15] = 1; + row[16] = 2; + row[17] = 3; + row[18] = 131; + return row; + } + + @Nested + @DisplayName("Inward stock report") + class InwardStockReportTests { + + @Test + @DisplayName("getInwardStockReport should number the rows of one facility in order") + void getInwardStockReport_shouldNumberRowsOfOneFacility() { + when(itemStockReportRepo.getItemStockEntryReportByFacilityID(START, END, 7)) + .thenReturn(List.of(new ItemStockEntryReport(), new ItemStockEntryReport())); + when(mapper.mapInwardStockReport(any(ItemStockEntryReport.class))) + .thenAnswer(inv -> new InwardStockReport()); + + assertTrue(service.getInwardStockReport(request(7)).contains("slNo")); + verify(itemStockReportRepo, never()).getItemStockEntryReport(any(), any()); + } + + @Test + @DisplayName("getInwardStockReport should cover every facility when none is named") + void getInwardStockReport_shouldCoverEveryFacilityWhenNoneNamed() { + when(itemStockReportRepo.getItemStockEntryReport(START, END)) + .thenReturn(List.of(new ItemStockEntryReport())); + when(mapper.mapInwardStockReport(any(ItemStockEntryReport.class))) + .thenAnswer(inv -> new InwardStockReport()); + + service.getInwardStockReport(request(null)); + + verify(itemStockReportRepo).getItemStockEntryReport(START, END); + } + } + + @Nested + @DisplayName("Expiry reports") + class ExpiryReportTests { + + private Object[] expiryRow() { + return new Object[] { "Main store", "Paracetamol", "Analgesic", "500mg", "B-1", + BigDecimal.valueOf(2.5d), EXPIRY, 40 }; + } + + @Test + @DisplayName("getExpiryReport should describe every batch expiring in the window at one facility") + void getExpiryReport_shouldDescribeBatchesOfOneFacility() { + when(itemStockReportRepo.getExpiryReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(expiryRow())); + + String report = service.getExpiryReport(request(7)); + + assertTrue(report.contains("Paracetamol")); + assertTrue(report.contains("B-1")); + } + + @Test + @DisplayName("getExpiryReport should cover every facility when none is named") + void getExpiryReport_shouldCoverEveryFacilityWhenNoneNamed() { + when(itemStockReportRepo.getExpiryReport(any(Date.class), any(Date.class))) + .thenReturn(rows(expiryRow())); + + service.getExpiryReport(request(null)); + + verify(itemStockReportRepo).getExpiryReport(any(Date.class), any(Date.class)); + } + + @Test + @DisplayName("getExpiryReport should skip an absent result row") + void getExpiryReport_shouldSkipAbsentRow() { + ArrayList results = rows(); + results.add(null); + when(itemStockReportRepo.getExpiryReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(results); + + assertEquals("[]", service.getExpiryReport(request(7))); + } + + @Test + @DisplayName("getExpiryReport should tolerate a row whose every column is null") + void getExpiryReport_shouldTolerateAllNullRow() { + when(itemStockReportRepo.getExpiryReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(new Object[8])); + + assertTrue(service.getExpiryReport(request(7)).contains("slNo")); + } + + @Test + @DisplayName("getShortExpiryReport should look ninety days ahead at one facility") + void getShortExpiryReport_shouldLookNinetyDaysAheadAtOneFacility() { + when(itemStockReportRepo.getShortExpiryReportByFacilityID(any(Date.class), eq(7))) + .thenReturn(rows(expiryRow())); + + assertTrue(service.getShortExpiryReport(request(7)).contains("Paracetamol")); + } + + @Test + @DisplayName("getShortExpiryReport should cover every facility when none is named") + void getShortExpiryReport_shouldCoverEveryFacilityWhenNoneNamed() { + when(itemStockReportRepo.getShortExpiryReport(any(Date.class))).thenReturn(rows(expiryRow())); + + service.getShortExpiryReport(request(null)); + + verify(itemStockReportRepo).getShortExpiryReport(any(Date.class)); + } + + @Test + @DisplayName("getShortExpiryReport should skip an absent result row") + void getShortExpiryReport_shouldSkipAbsentRow() { + ArrayList results = rows(); + results.add(null); + when(itemStockReportRepo.getShortExpiryReportByFacilityID(any(Date.class), eq(7))) + .thenReturn(results); + + assertEquals("[]", service.getShortExpiryReport(request(7))); + } + } + + @Nested + @DisplayName("Consumption report") + class ConsumptionReportTests { + + private ItemStockExitReport exitRequest(Integer facilityID) { + ItemStockExitReport request = new ItemStockExitReport(); + request.setFacilityID(facilityID); + request.setStartDate(START); + request.setEndDate(END); + return request; + } + + private Object[] consumptionRow() { + Object[] row = new Object[40]; + row[4] = "Main store"; + row[11] = "Paracetamol"; + row[14] = "Analgesic"; + row[16] = "B-1"; + row[19] = BigDecimal.valueOf(2.5d); + row[20] = EXPIRY; + row[23] = 6; + row[26] = "PatientIssue"; + row[32] = Timestamp.valueOf("2025-01-15 11:00:00"); + row[38] = BigInteger.valueOf(101L); + row[39] = "Jane Doe"; + return row; + } + + @Test + @DisplayName("getConsumptionReport should describe every consumption at one facility") + void getConsumptionReport_shouldDescribeConsumptionsOfOneFacility() { + when(itemStockReportRepo.getItemStockExitReportByFacilityID(START, END, 7)) + .thenReturn(rows(consumptionRow())); + + String report = service.getConsumptionReport(exitRequest(7)); + + assertTrue(report.contains("Paracetamol")); + assertTrue(report.contains("Jane Doe")); + assertTrue(report.contains("101")); + } + + @Test + @DisplayName("getConsumptionReport should cover every facility when none is named") + void getConsumptionReport_shouldCoverEveryFacilityWhenNoneNamed() { + when(itemStockReportRepo.getItemStockExitReport(START, END)).thenReturn(rows(consumptionRow())); + + service.getConsumptionReport(exitRequest(null)); + + verify(itemStockReportRepo).getItemStockExitReport(START, END); + } + + @Test + @DisplayName("getConsumptionReport should skip a row that is too short to read") + void getConsumptionReport_shouldSkipShortRow() { + when(itemStockReportRepo.getItemStockExitReportByFacilityID(START, END, 7)) + .thenReturn(rows(new Object[10])); + + assertEquals("[]", service.getConsumptionReport(exitRequest(7))); + } + + @Test + @DisplayName("getConsumptionReport should report a blank beneficiary id when the row carries none") + void getConsumptionReport_shouldReportBlankBeneficiaryIdWhenAbsent() { + Object[] row = consumptionRow(); + row[38] = null; + when(itemStockReportRepo.getItemStockExitReportByFacilityID(START, END, 7)).thenReturn(rows(row)); + + assertTrue(service.getConsumptionReport(exitRequest(7)).contains("\"beneficiaryID\":\"\"")); + } + } + + @Nested + @DisplayName("Beneficiary drug issue report") + class BenDrugIssueReportTests { + + private PatientIssueExitReport issueRequest(Integer facilityID) { + PatientIssueExitReport request = new PatientIssueExitReport(); + request.setFacilityID(facilityID); + request.setStartDate(START); + request.setEndDate(END); + return request; + } + + private Object[] issueRow() { + Object[] row = new Object[36]; + row[0] = 1L; + row[1] = 501L; + row[2] = 601L; + row[3] = 11; + row[4] = "Paracetamol"; + row[7] = "Analgesic"; + row[8] = "500mg"; + row[9] = "B-1"; + row[13] = EXPIRY; + row[14] = 6; + row[22] = 7; + row[23] = "Jane Doe"; + row[24] = 34; + row[25] = "Female"; + row[35] = Timestamp.valueOf("2025-01-15 11:00:00"); + return row; + } + + @Test + @DisplayName("getBenDrugIssueReport should map every dispensed line at one facility") + void getBenDrugIssueReport_shouldMapDispensedLinesOfOneFacility() { + when(itemStockReportRepo.getPatientIssueExitReportByFacilityID(START, END, 7)) + .thenReturn(rows(issueRow())); + when(mapper.mapBenDrugIssueReport(any(PatientIssueExitReport.class))) + .thenAnswer(inv -> new BenDrugIssueReport()); + + assertTrue(service.getBenDrugIssueReport(issueRequest(7)).contains("slNo")); + verify(mapper).mapBenDrugIssueReport(any(PatientIssueExitReport.class)); + } + + @Test + @DisplayName("getBenDrugIssueReport should cover every facility when none is named") + void getBenDrugIssueReport_shouldCoverEveryFacilityWhenNoneNamed() { + when(itemStockReportRepo.getPatientIssueExitReport(START, END)).thenReturn(rows(issueRow())); + when(mapper.mapBenDrugIssueReport(any(PatientIssueExitReport.class))) + .thenAnswer(inv -> new BenDrugIssueReport()); + + service.getBenDrugIssueReport(issueRequest(null)); + + verify(itemStockReportRepo).getPatientIssueExitReport(START, END); + } + + @Test + @DisplayName("getBenDrugIssueReport should skip an absent result row") + void getBenDrugIssueReport_shouldSkipAbsentRow() { + ArrayList results = rows(); + results.add(null); + when(itemStockReportRepo.getPatientIssueExitReportByFacilityID(START, END, 7)).thenReturn(results); + + assertEquals("[]", service.getBenDrugIssueReport(issueRequest(7))); + } + } + + @Nested + @DisplayName("Daily, monthly and yearly stock reports") + class PeriodicStockReportTests { + + @Test + @DisplayName("getDailyStockDetailsReport should describe each batch movement of the day") + void getDailyStockDetailsReport_shouldDescribeBatchMovements() { + when(itemStockReportRepo.getDailyStockDetailReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(stockDetailRow())); + + String report = service.getDailyStockDetailsReport(request(7)); + + assertTrue(report.contains("Paracetamol")); + assertTrue(report.contains("Main store")); + assertTrue(report.contains("B-1")); + } + + @Test + @DisplayName("getDailyStockDetailsReport should default every absent count to zero") + void getDailyStockDetailsReport_shouldDefaultAbsentCountsToZero() { + when(itemStockReportRepo.getDailyStockDetailReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(new Object[19])); + + String report = service.getDailyStockDetailsReport(request(7)); + + assertTrue(report.contains("\"openingStock\":\"0\"")); + assertTrue(report.contains("\"closingStock\":\"0\"")); + } + + @Test + @DisplayName("getDailyStockDetailsReport should skip an absent result row") + void getDailyStockDetailsReport_shouldSkipAbsentRow() { + ArrayList results = rows(); + results.add(null); + when(itemStockReportRepo.getDailyStockDetailReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(results); + + assertEquals("[]", service.getDailyStockDetailsReport(request(7))); + } + + @Test + @DisplayName("getDailyStockSummaryReport should roll the day up per item") + void getDailyStockSummaryReport_shouldRollUpPerItem() { + Object[] row = new Object[13]; + row[2] = "Paracetamol"; + row[3] = "Main store"; + row[4] = "Analgesic"; + row[5] = 100; + row[6] = 40; + row[7] = 1; + row[8] = 6; + row[9] = 1; + row[10] = 2; + row[11] = 3; + row[12] = 131; + when(itemStockReportRepo.getDailyStockSummaryReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(row)); + + String report = service.getDailyStockSummaryReport(request(7)); + + assertTrue(report.contains("Paracetamol")); + assertTrue(report.contains("\"closingStock\":\"131\"")); + } + + @Test + @DisplayName("getDailyStockSummaryReport should default every absent count to zero") + void getDailyStockSummaryReport_shouldDefaultAbsentCountsToZero() { + when(itemStockReportRepo.getDailyStockSummaryReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(new Object[13])); + + assertTrue(service.getDailyStockSummaryReport(request(7)).contains("\"openingStock\":\"0\"")); + } + + @Test + @DisplayName("getDailyStockSummaryReport should skip an absent result row") + void getDailyStockSummaryReport_shouldSkipAbsentRow() { + ArrayList results = rows(); + results.add(null); + when(itemStockReportRepo.getDailyStockSummaryReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(results); + + assertEquals("[]", service.getDailyStockSummaryReport(request(7))); + } + + @Test + @DisplayName("getMonthlyReport should cover the whole of the requested month") + void getMonthlyReport_shouldCoverWholeMonth() { + ItemStockEntryReport monthRequest = request(7); + monthRequest.setYear(2025); + monthRequest.setMonth(0); + monthRequest.setMonthName("January"); + when(itemStockReportRepo.getDailyStockDetailReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(stockDetailRow())); + + String report = service.getMonthlyReport(monthRequest); + + assertTrue(report.contains("January")); + assertTrue(report.contains("Paracetamol")); + } + + @Test + @DisplayName("getMonthlyReport should default every absent count to zero") + void getMonthlyReport_shouldDefaultAbsentCountsToZero() { + ItemStockEntryReport monthRequest = request(7); + monthRequest.setYear(2025); + monthRequest.setMonth(0); + when(itemStockReportRepo.getDailyStockDetailReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(new Object[19])); + + assertTrue(service.getMonthlyReport(monthRequest).contains("\"openingStock\":\"0\"")); + } + + @Test + @DisplayName("getMonthlyReport should skip an absent result row") + void getMonthlyReport_shouldSkipAbsentRow() { + ItemStockEntryReport monthRequest = request(7); + monthRequest.setYear(2025); + monthRequest.setMonth(0); + ArrayList results = rows(); + results.add(null); + when(itemStockReportRepo.getDailyStockDetailReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(results); + + assertEquals("[]", service.getMonthlyReport(monthRequest)); + } + + @Test + @DisplayName("getYearlyReport should cover the whole of the requested year") + void getYearlyReport_shouldCoverWholeYear() { + ItemStockEntryReport yearRequest = request(7); + yearRequest.setYear(2025); + when(itemStockReportRepo.getDailyStockDetailReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(stockDetailRow())); + + String report = service.getYearlyReport(yearRequest); + + assertTrue(report.contains("2025")); + assertTrue(report.contains("Paracetamol")); + } + + @Test + @DisplayName("getYearlyReport should default every absent count to zero") + void getYearlyReport_shouldDefaultAbsentCountsToZero() { + ItemStockEntryReport yearRequest = request(7); + yearRequest.setYear(2025); + when(itemStockReportRepo.getDailyStockDetailReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(rows(new Object[19])); + + assertTrue(service.getYearlyReport(yearRequest).contains("\"openingStock\":\"0\"")); + } + + @Test + @DisplayName("getYearlyReport should skip an absent result row") + void getYearlyReport_shouldSkipAbsentRow() { + ItemStockEntryReport yearRequest = request(7); + yearRequest.setYear(2025); + ArrayList results = rows(); + results.add(null); + when(itemStockReportRepo.getDailyStockDetailReportByFacilityID(any(Date.class), any(Date.class), eq(7))) + .thenReturn(results); + + assertEquals("[]", service.getYearlyReport(yearRequest)); + } + } + + @Nested + @DisplayName("Transit report") + class TransitReportTests { + + private Object[] transitRow() { + return new Object[] { "Paracetamol", "B-1", BigDecimal.valueOf(2.5d), EXPIRY, "Main store", "Sub store", + Timestamp.valueOf("2025-01-10 09:00:00"), Timestamp.valueOf("2025-01-11 09:00:00") }; + } + + @Test + @DisplayName("getTransitReport should describe every transfer touching one facility") + void getTransitReport_shouldDescribeTransfersOfOneFacility() { + when(itemStockReportRepo.getTransitReportByFacilityID(START, END, 7)).thenReturn(rows(transitRow())); + + String report = service.getTransitReport(request(7)); + + assertTrue(report.contains("Paracetamol")); + assertTrue(report.contains("Sub store")); + } + + @Test + @DisplayName("getTransitReport should cover every facility when none is named") + void getTransitReport_shouldCoverEveryFacilityWhenNoneNamed() { + when(itemStockReportRepo.getTransitReport(START, END)).thenReturn(rows(transitRow())); + + service.getTransitReport(request(null)); + + verify(itemStockReportRepo).getTransitReport(START, END); + } + + @Test + @DisplayName("getTransitReport should skip an absent result row") + void getTransitReport_shouldSkipAbsentRow() { + ArrayList results = rows(); + results.add(null); + when(itemStockReportRepo.getTransitReportByFacilityID(START, END, 7)).thenReturn(results); + + assertEquals("[]", service.getTransitReport(request(7))); + } + } +} diff --git a/src/test/java/com/iemr/inventory/service/stockEntry/StockEntryServiceImplTest.java b/src/test/java/com/iemr/inventory/service/stockEntry/StockEntryServiceImplTest.java new file mode 100644 index 00000000..26332885 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/stockEntry/StockEntryServiceImplTest.java @@ -0,0 +1,525 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.stockEntry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +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.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import com.iemr.inventory.data.items.ItemMaster; +import com.iemr.inventory.data.items.M_ItemCategory; +import com.iemr.inventory.data.stockExit.ItemStockExit; +import com.iemr.inventory.data.stockentry.AllocateItemMap; +import com.iemr.inventory.data.stockentry.ItemBatchList; +import com.iemr.inventory.data.stockentry.ItemMasterWithQuantityMap; +import com.iemr.inventory.data.stockentry.ItemStockEntry; +import com.iemr.inventory.data.stockentry.ItemStockEntryinput; +import com.iemr.inventory.data.stockentry.PhysicalStockEntry; +import com.iemr.inventory.mapper.stockExit.ItemBatchListMap; +import com.iemr.inventory.mapper.stockExit.ItemMasterWithQuantityMapper; +import com.iemr.inventory.repo.stockEntry.ItemStockEntryRepo; +import com.iemr.inventory.repo.stockEntry.PhysicalStockEntryRepo; +import com.iemr.inventory.repository.itemfacilitymapping.M_itemfacilitymappingRepo; +import com.iemr.inventory.service.item.ItemService; +import com.iemr.inventory.utils.exception.InventoryException; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("StockEntryServiceImpl Test Suite") +class StockEntryServiceImplTest { + + @Mock + private PhysicalStockEntryRepo physicalStockEntryRepo; + @Mock + private ItemStockEntryRepo itemStockEntryRepo; + @Mock + private ItemService itemService; + @Mock + private M_itemfacilitymappingRepo m_itemfacilitymappingRepo; + @Mock + private ItemMasterWithQuantityMapper itemMasterWithQuantityMapper; + @Mock + private ItemBatchListMap itemBatchListMap; + + @InjectMocks + private StockEntryServiceImpl service; + + private static ItemStockEntry entry(Integer id, Integer itemID, String batchNo, Integer inHand) { + ItemStockEntry entry = new ItemStockEntry(); + if (id != null) { + entry.setItemStockEntryID(id); + } + entry.setItemID(itemID); + entry.setBatchNo(batchNo); + entry.setQuantity(inHand); + entry.setQuantityInHand(inHand); + return entry; + } + + private static PhysicalStockEntry physicalEntry(ItemStockEntry... items) { + PhysicalStockEntry physical = new PhysicalStockEntry(); + physical.setPhyEntryID(77L); + physical.setFacilityID(7); + physical.setVanID(4L); + physical.setParkingPlaceID(2L); + physical.setCreatedBy("tester"); + physical.setItemStockEntry(new ArrayList<>(List.of(items))); + return physical; + } + + private static ItemStockExit exit(Long stockEntryID, Integer itemID, Integer quantity) { + ItemStockExit exit = new ItemStockExit(); + exit.setItemStockEntryID(stockEntryID); + exit.setItemID(itemID); + exit.setQuantity(quantity); + exit.setCreatedBy("tester"); + return exit; + } + + private static ItemMaster itemWithIssueType(Integer itemID, String issueType) { + M_ItemCategory category = new M_ItemCategory(); + category.setIssueType(issueType); + + ItemMaster item = new ItemMaster(); + item.setItemID(itemID); + item.setItemName("Paracetamol"); + item.setFacilityID(7); + item.setItemCategory(category); + return item; + } + + private static ArrayList rows(Object[]... values) { + ArrayList list = new ArrayList<>(); + for (Object[] value : values) { + list.add(value); + } + return list; + } + + @Test + @DisplayName("savePhysicalStockEntry should stamp the header details onto every batch it books in") + void savePhysicalStockEntry_shouldStampHeaderOntoBatches() throws Exception { + PhysicalStockEntry physical = physicalEntry(entry(null, 11, "B-1", 100)); + + PhysicalStockEntry result = service.savePhysicalStockEntry(physical); + + ItemStockEntry booked = result.getItemStockEntry().get(0); + assertEquals(7, result.getSyncFacilityID()); + assertEquals(77L, booked.getEntryTypeID()); + assertEquals("physicalStockEntry", booked.getEntryType()); + assertEquals(100, booked.getQuantityInHand()); + assertEquals(4L, booked.getVanID()); + assertEquals(2L, booked.getParkingPlaceID()); + assertEquals("tester", booked.getCreatedBy()); + assertEquals(7, booked.getSyncFacilityID()); + verify(itemStockEntryRepo).saveAll(physical.getItemStockEntry()); + verify(physicalStockEntryRepo).updatePhysicalStockEntryVanSerialNo(); + verify(itemStockEntryRepo).updateItemStockEntryVanSerialNo(); + } + + @Test + @DisplayName("savePhysicalStockEntry should refuse a batch the facility already holds") + void savePhysicalStockEntry_shouldRefuseDuplicateBatch() { + PhysicalStockEntry physical = physicalEntry(entry(null, 11, "B-1", 100)); + when(itemStockEntryRepo.existsByFacilityIDAndItemIDAndBatchNoAndExpiryDateAndEntryTypeAndDeletedFalse( + eq(7), eq(11), eq("B-1"), any(), eq("physicalStockEntry"))).thenReturn(true); + + InventoryException ex = assertThrows(InventoryException.class, + () -> service.savePhysicalStockEntry(physical)); + assertTrue(ex.getMessage().contains("Duplicate stock entry")); + verify(itemStockEntryRepo, never()).saveAll(anyList()); + } + + @Test + @DisplayName("savePhysicalStockEntry should check the duplicate against the batch's own facility when it has one") + void savePhysicalStockEntry_shouldUseBatchFacilityWhenPresent() throws Exception { + ItemStockEntry batch = entry(null, 11, "B-1", 100); + batch.setFacilityID(9); + PhysicalStockEntry physical = physicalEntry(batch); + + service.savePhysicalStockEntry(physical); + + verify(itemStockEntryRepo).existsByFacilityIDAndItemIDAndBatchNoAndExpiryDateAndEntryTypeAndDeletedFalse( + eq(9), eq(11), eq("B-1"), any(), eq("physicalStockEntry")); + } + + @Test + @DisplayName("getItemBatchForStoreID should ask for the live, unexpired batches of the item") + void getItemBatchForStoreID_shouldAskForLiveBatches() { + ItemStockEntry probe = entry(null, 11, null, null); + probe.setFacilityID(7); + List batches = List.of(entry(601, 11, "B-1", 40)); + when(itemStockEntryRepo.findByFacilityIDAndItemIDAndQuantityInHandGreaterThanAndDeletedAndExpiryDateAfter( + eq(7), eq(11), eq(0), eq(false), any(Date.class))).thenReturn(batches); + + assertSame(batches, service.getItemBatchForStoreID(probe)); + } + + @Test + @DisplayName("getAllItemBatchForStoreID should delegate the quantity roll-up to the repository") + void getAllItemBatchForStoreID_shouldDelegate() { + Long[] stockIDs = { 601L }; + ArrayList quantities = rows(new Object[] { 601L, 40L }); + when(itemStockEntryRepo.getQuantityOfStock(stockIDs, 7)).thenReturn(quantities); + + assertSame(quantities, service.getAllItemBatchForStoreID(7, stockIDs)); + } + + @Test + @DisplayName("updateStocks should total the rows each exit line updated") + void updateStocks_shouldTotalUpdatedRows() { + ItemStockExit first = exit(601L, 11, 6); + first.setFacilityID(7); + ItemStockExit second = exit(602L, 12, 3); + second.setFacilityID(7); + when(itemStockEntryRepo.updateStock(7, 601L, 6)).thenReturn(1); + when(itemStockEntryRepo.updateStock(7, 602L, 3)).thenReturn(1); + + assertEquals(2, service.updateStocks(List.of(first, second))); + } + + @Test + @DisplayName("the three ordered batch lookups should each use their own repository ordering") + void orderedBatchLookups_shouldUseTheirOwnOrdering() { + Date now = new Date(); + List byEntryAsc = List.of(entry(601, 11, "B-1", 40)); + List byEntryDesc = List.of(entry(602, 11, "B-2", 40)); + List byExpiryAsc = List.of(entry(603, 11, "B-3", 40)); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByAsc( + 7, 11, false, 0, now)).thenReturn(byEntryAsc); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByDesc( + 7, 11, false, 0, now)).thenReturn(byEntryDesc); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByExpiryDateAsc( + 7, 11, false, 0, now)).thenReturn(byExpiryAsc); + + assertSame(byEntryAsc, service.getItemStockForStoreIDOrderByEntryDateAsc(7, 11, now)); + assertSame(byEntryDesc, service.getItemStockForStoreIDOrderByEntryDateDesc(7, 11, now)); + assertSame(byExpiryAsc, service.getItemStockForStoreIDOrderByExpiryDateAsc(7, 11, now)); + } + + private void givenBatchMapperEchoes() { + when(itemBatchListMap.getItemStockExitMapList(anyList())).thenAnswer(invocation -> { + List input = invocation.getArgument(0); + List mapped = new ArrayList<>(); + for (ItemStockEntry stock : input) { + ItemBatchList batch = new ItemBatchList(); + batch.setBatchNo(stock.getBatchNo()); + batch.setQuantity(stock.getQuantity()); + batch.setExpiryDate(stock.getExpiryDate()); + mapped.add(batch); + } + return mapped; + }); + } + + @Test + @DisplayName("getItemStockFromItemID should allocate across batches until the requested quantity is met") + void getItemStockFromItemID_shouldAllocateAcrossBatches() throws Exception { + when(itemService.getItemMasterCatByID(11)).thenReturn(itemWithIssueType(11, "First in First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByAsc( + eq(7), eq(11), eq(false), eq(0), any(Date.class))) + .thenReturn(new ArrayList<>(List.of(entry(601, 11, "B-1", 4), entry(602, 11, "B-2", 10)))); + givenBatchMapperEchoes(); + + List result = service.getItemStockFromItemID(7, List.of(exit(null, 11, 6))); + + assertEquals(1, result.size()); + assertEquals("Paracetamol", result.get(0).getItemName()); + List batches = result.get(0).getItemBatchList(); + assertEquals(2, batches.size()); + assertEquals(4, batches.get(0).getQuantity(), "the first batch is drained"); + assertEquals(2, batches.get(1).getQuantity(), "the second batch covers only the shortfall"); + } + + @Test + @DisplayName("getItemStockFromItemID should pick the expiry ordering for a first-expiry-first-out item") + void getItemStockFromItemID_shouldPickExpiryOrdering() throws Exception { + when(itemService.getItemMasterCatByID(11)).thenReturn(itemWithIssueType(11, "First Expiry First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByExpiryDateAsc( + eq(7), eq(11), eq(false), eq(0), any(Date.class))) + .thenReturn(new ArrayList<>(List.of(entry(601, 11, "B-1", 40)))); + givenBatchMapperEchoes(); + + service.getItemStockFromItemID(7, List.of(exit(null, 11, 6))); + + verify(itemStockEntryRepo) + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByExpiryDateAsc( + eq(7), eq(11), eq(false), eq(0), any(Date.class)); + } + + @Test + @DisplayName("getItemStockFromItemID should pick the newest-first ordering for a last-in-first-out item") + void getItemStockFromItemID_shouldPickNewestFirstOrdering() throws Exception { + when(itemService.getItemMasterCatByID(11)).thenReturn(itemWithIssueType(11, "Last in First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByDesc( + eq(7), eq(11), eq(false), eq(0), any(Date.class))) + .thenReturn(new ArrayList<>(List.of(entry(601, 11, "B-1", 40)))); + givenBatchMapperEchoes(); + + service.getItemStockFromItemID(7, List.of(exit(null, 11, 6))); + + verify(itemStockEntryRepo) + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByDesc( + eq(7), eq(11), eq(false), eq(0), any(Date.class)); + } + + @Test + @DisplayName("getItemStockFromItemID should fall back to the oldest-first ordering for an unconfigured item") + void getItemStockFromItemID_shouldFallBackToOldestFirst() throws Exception { + when(itemService.getItemMasterCatByID(11)).thenReturn(itemWithIssueType(11, null)); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByAsc( + eq(7), eq(11), eq(false), eq(0), any(Date.class))) + .thenReturn(new ArrayList<>(List.of(entry(601, 11, "B-1", 40)))); + givenBatchMapperEchoes(); + + service.getItemStockFromItemID(7, List.of(exit(null, 11, 6))); + + verify(itemStockEntryRepo) + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByAsc( + eq(7), eq(11), eq(false), eq(0), any(Date.class)); + } + + @Test + @DisplayName("getItemStockFromItemID should fall back to the oldest-first ordering for an unknown issue type") + void getItemStockFromItemID_shouldFallBackForUnknownIssueType() throws Exception { + when(itemService.getItemMasterCatByID(11)).thenReturn(itemWithIssueType(11, "Something Else")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByAsc( + eq(7), eq(11), eq(false), eq(0), any(Date.class))) + .thenReturn(new ArrayList<>(List.of(entry(601, 11, "B-1", 40)))); + givenBatchMapperEchoes(); + + assertEquals(1, service.getItemStockFromItemID(7, List.of(exit(null, 11, 6))).size()); + } + + @Test + @DisplayName("getItemStockFromItemID should push the cut-off date out by the requested course duration") + void getItemStockFromItemID_shouldPushCutOffByCourseDuration() throws Exception { + when(itemService.getItemMasterCatByID(11)).thenReturn(itemWithIssueType(11, "First in First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByAsc( + eq(7), eq(11), eq(false), eq(0), any(Date.class))) + .thenReturn(new ArrayList<>(List.of(entry(601, 11, "B-1", 40)))); + givenBatchMapperEchoes(); + + for (String unit : new String[] { "Day(s)", "Month(s)", "Week(s)", "Hour(s)" }) { + ItemStockExit request = exit(null, 11, 6); + request.setDuration(2); + request.setDurationUnit(unit); + + assertEquals(1, service.getItemStockFromItemID(7, List.of(request)).size(), + "a course measured in " + unit + " must still allocate"); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(Date.class); + verify(itemStockEntryRepo, org.mockito.Mockito.atLeastOnce()) + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByAsc( + eq(7), eq(11), eq(false), eq(0), captor.capture()); + assertTrue(captor.getValue().after(new Date(System.currentTimeMillis() - 1000))); + } + + @Test + @DisplayName("getItemStockFromItemID should report how many days each allocated batch has left") + void getItemStockFromItemID_shouldReportDaysToExpiry() throws Exception { + ItemStockEntry batch = entry(601, 11, "B-1", 40); + // Half a day past the ten-day mark, so the whole-day division cannot land on nine. + batch.setExpiryDate(new java.sql.Date(System.currentTimeMillis() + 10L * 86400000L + 43200000L)); + when(itemService.getItemMasterCatByID(11)).thenReturn(itemWithIssueType(11, "First in First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanAndExpiryDateAfterOrderByCreatedByAsc( + eq(7), eq(11), eq(false), eq(0), any(Date.class))) + .thenReturn(new ArrayList<>(List.of(batch))); + givenBatchMapperEchoes(); + + ItemBatchList allocated = service.getItemStockFromItemID(7, List.of(exit(null, 11, 6))) + .get(0).getItemBatchList().get(0); + + assertEquals(10L, allocated.getExpiresIn()); + } + + @Test + @DisplayName("saveItemStockFromStockTransfer should book the transferred quantity into the receiving store") + void saveItemStockFromStockTransfer_shouldBookIntoReceivingStore() { + ItemStockEntry sending = entry(601, 11, "B-1", 40); + sending.setExpiryDate(java.sql.Date.valueOf("2026-01-31")); + when(itemStockEntryRepo.findByFacilityIDAndItemStockEntryIDIn(eq(1), any(Long[].class))) + .thenReturn(List.of(sending)); + + List result = service.saveItemStockFromStockTransfer( + List.of(exit(601L, 11, 6)), 88L, "stockTransfer", 1, 2, 4L); + + assertEquals(1, result.size()); + ItemStockEntry booked = result.get(0); + assertEquals(2, booked.getFacilityID()); + assertEquals(6, booked.getQuantity()); + assertEquals(6, booked.getQuantityInHand()); + assertEquals(11, booked.getItemID()); + assertEquals("B-1", booked.getBatchNo()); + assertEquals("stockTransfer", booked.getEntryType()); + assertEquals(88L, booked.getEntryTypeID()); + assertEquals(1, booked.getSyncFacilityID()); + assertEquals(4L, booked.getVanID()); + verify(itemStockEntryRepo).updateItemStockEntryVanSerialNo(); + } + + @Test + @DisplayName("getPhysicalStockEntry should widen the window to whole days before querying") + void getPhysicalStockEntry_shouldWidenWindowToWholeDays() { + ItemStockEntryinput input = new ItemStockEntryinput(); + input.setFacilityID(7); + input.setFromDate(Timestamp.valueOf("2025-01-01 08:30:00")); + input.setToDate(Timestamp.valueOf("2025-01-31 08:30:00")); + List found = List.of(physicalEntry()); + when(physicalStockEntryRepo.findByFacilityIDAndCreatedDateBetweenOrderByCreatedDateDesc( + 7, Timestamp.valueOf("2025-01-01 00:00:00"), Timestamp.valueOf("2025-01-31 23:59:00"))) + .thenReturn(found); + + assertSame(found, service.getPhysicalStockEntry(input)); + } + + @Test + @DisplayName("getPhysicalStockEntry should return nothing when the window is incomplete") + void getPhysicalStockEntry_shouldReturnNothingForIncompleteWindow() { + assertTrue(service.getPhysicalStockEntry(new ItemStockEntryinput()).isEmpty()); + + ItemStockEntryinput facilityOnly = new ItemStockEntryinput(); + facilityOnly.setFacilityID(7); + assertTrue(service.getPhysicalStockEntry(facilityOnly).isEmpty()); + + ItemStockEntryinput noToDate = new ItemStockEntryinput(); + noToDate.setFacilityID(7); + noToDate.setFromDate(Timestamp.valueOf("2025-01-01 08:30:00")); + assertTrue(service.getPhysicalStockEntry(noToDate).isEmpty()); + } + + @Test + @DisplayName("getItemMastersPartialSearch should resolve the matched ids into batches that still hold stock") + void getItemMastersPartialSearch_shouldResolveMatchedIds() { + when(m_itemfacilitymappingRepo.getItemforStoreLikeItemName(7, "Para")) + .thenReturn(rows(new Object[] { 11, "Paracetamol" })); + List batches = List.of(entry(601, 11, "B-1", 40)); + when(itemStockEntryRepo.findByItemIDInAndQuantityInHandGreaterThanAndFacilityID(any(Integer[].class), eq(0), eq(7))) + .thenReturn(batches); + + assertSame(batches, service.getItemMastersPartialSearch("Para", 7)); + } + + @Test + @DisplayName("getItemMastersPartialSearch should return nothing when the search matches no item") + void getItemMastersPartialSearch_shouldReturnNothingWhenNoMatch() { + when(m_itemfacilitymappingRepo.getItemforStoreLikeItemName(7, "zzz")).thenReturn(rows()); + + assertTrue(service.getItemMastersPartialSearch("zzz", 7).isEmpty()); + } + + @Test + @DisplayName("getPhysicalStockEntryItems should look the booked batches up by the header's sync keys") + void getPhysicalStockEntryItems_shouldLookUpBySyncKeys() { + PhysicalStockEntry header = physicalEntry(); + header.setVanSerialNo(99L); + header.setSyncFacilityID(7); + List batches = List.of(entry(601, 11, "B-1", 40)); + when(physicalStockEntryRepo.findById(77L)).thenReturn(Optional.of(header)); + when(itemStockEntryRepo.findByEntryTypeIDAndSyncFacilityIDAndEntryType(99L, 7, "physicalStockEntry")) + .thenReturn(batches); + + assertSame(batches, service.getPhysicalStockEntryItems(77L)); + } + + @Test + @DisplayName("getItemwithQuantityPartialSearch should fold the aggregated quantity onto each matched batch") + void getItemwithQuantityPartialSearch_shouldFoldAggregatedQuantity() { + ItemStockEntry batch = entry(601, 11, "B-1", 40); + when(m_itemfacilitymappingRepo.getItemforStoreLikeItemName(7, "Para")) + .thenReturn(rows(new Object[] { 11, "Paracetamol" })); + when(itemStockEntryRepo.findByItemIDInQuantityInHandGreaterThanForFacilityID( + any(Integer[].class), eq(0L), eq(7), any(Date.class))) + .thenReturn(rows(new Object[] { batch, 55L })); + List mapped = List.of(new ItemMasterWithQuantityMap()); + when(itemMasterWithQuantityMapper.getItemStockExitMapList(anyList())).thenReturn(mapped); + + assertSame(mapped, service.getItemwithQuantityPartialSearch("Para", 7)); + assertEquals(55, batch.getQuantityInHand(), "the aggregated quantity replaces the per-row one"); + } + + @Test + @DisplayName("getItemwithQuantityPartialSearch should map an empty list when the search matches no item") + void getItemwithQuantityPartialSearch_shouldMapEmptyListWhenNoMatch() { + when(m_itemfacilitymappingRepo.getItemforStoreLikeItemName(7, "zzz")).thenReturn(rows()); + when(itemMasterWithQuantityMapper.getItemStockExitMapList(anyList())).thenReturn(List.of()); + + assertTrue(service.getItemwithQuantityPartialSearch("zzz", 7).isEmpty()); + } + + @Test + @DisplayName("getItemMastersPartialSearchWithZero should include the batches that hold no stock left") + void getItemMastersPartialSearchWithZero_shouldIncludeEmptyBatches() { + when(m_itemfacilitymappingRepo.getItemforStoreLikeItemName(7, "Para")) + .thenReturn(rows(new Object[] { 11, "Paracetamol" })); + List batches = List.of(entry(601, 11, "B-1", 0)); + when(itemStockEntryRepo.findByItemIDInAndFacilityIDOrderByItemStockEntryIDDesc(any(Integer[].class), eq(7))) + .thenReturn(batches); + + assertSame(batches, service.getItemMastersPartialSearchWithZero("Para", 7)); + } + + @Test + @DisplayName("getItemMastersPartialSearchWithZero should return nothing when the search matches no item") + void getItemMastersPartialSearchWithZero_shouldReturnNothingWhenNoMatch() { + when(m_itemfacilitymappingRepo.getItemforStoreLikeItemName(7, "zzz")).thenReturn(rows()); + + assertTrue(service.getItemMastersPartialSearchWithZero("zzz", 7).isEmpty()); + } +} diff --git a/src/test/java/com/iemr/inventory/service/stockExit/StockExitServiceImplTest.java b/src/test/java/com/iemr/inventory/service/stockExit/StockExitServiceImplTest.java new file mode 100644 index 00000000..75d85c83 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/stockExit/StockExitServiceImplTest.java @@ -0,0 +1,479 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.stockExit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +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.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +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.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import com.iemr.inventory.data.stockExit.ItemStockExit; +import com.iemr.inventory.data.stockExit.ItemStockExitMap; +import com.iemr.inventory.data.stockExit.StoreSelfConsumption; +import com.iemr.inventory.data.stockExit.T_PatientIssue; +import com.iemr.inventory.data.stockExit.T_StockTransfer; +import com.iemr.inventory.data.stockentry.ItemStockEntry; +import com.iemr.inventory.data.stockentry.ItemStockEntryinput; +import com.iemr.inventory.data.user.M_User; +import com.iemr.inventory.mapper.stockExit.ItemStockExitMapper; +import com.iemr.inventory.repo.stockEntry.ItemStockEntryRepo; +import com.iemr.inventory.repo.stockExit.ItemStockExitRepo; +import com.iemr.inventory.repo.stockExit.PatientIssueRepo; +import com.iemr.inventory.repo.stockExit.StockTransferRepo; +import com.iemr.inventory.repo.stockExit.StoreSelfConsumptionRepo; +import com.iemr.inventory.repo.users.UserLoginRepo; +import com.iemr.inventory.service.item.ItemService; +import com.iemr.inventory.service.stockEntry.StockEntryService; +import com.iemr.inventory.utils.exception.InventoryException; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("StockExitServiceImpl Test Suite") +class StockExitServiceImplTest { + + @Mock + private StockEntryService stockEntryService; + @Mock + private ItemStockExitRepo itemStockExitRepo; + @Mock + private ItemStockEntryRepo itemStockEntryRepo; + @Mock + private PatientIssueRepo patientIssueRepo; + @Mock + private UserLoginRepo userLoginRepo; + @Mock + private StoreSelfConsumptionRepo storeSelfConsumptionRepo; + @Mock + private StockTransferRepo stockTransferRepo; + @Mock + private ItemService itemService; + @Mock + private ItemStockExitMapper itemStockExitMapper; + + @InjectMocks + private StockExitServiceImpl service; + + private static ItemStockExit exitLine(Long stockEntryID, Integer quantity) { + ItemStockExit exit = new ItemStockExit(); + exit.setItemStockEntryID(stockEntryID); + exit.setQuantity(quantity); + return exit; + } + + /** + * Builds a stock-in-hand row in the column order getAllItemBatchForStoreID returns: + * index 1 keys back to the requested batch, index 3 is the quantity in hand and index 4 + * is the batch to draw from. + */ + private static Object[] stockRow(Long requestedEntryID, Integer inHand, Long drawFromEntryID) { + return new Object[] { null, requestedEntryID, null, inHand, drawFromEntryID }; + } + + private static ArrayList rows(Object[]... values) { + ArrayList list = new ArrayList<>(); + for (Object[] value : values) { + list.add(value); + } + return list; + } + + private static T_PatientIssue patientIssue(List lines) { + T_PatientIssue issue = new T_PatientIssue(); + issue.setPatientIssueID(88L); + issue.setFacilityID(7); + issue.setBenRegID(101L); + issue.setVisitCode(5001L); + issue.setCreatedBy("pharma.user"); + issue.setItemStockExit(lines); + return issue; + } + + private static ItemStockEntryinput window(Integer facilityID, String from, String to) { + ItemStockEntryinput input = new ItemStockEntryinput(); + input.setFacilityID(facilityID); + input.setFromDate(from == null ? null : Timestamp.valueOf(from)); + input.setToDate(to == null ? null : Timestamp.valueOf(to)); + return input; + } + + @Test + @DisplayName("issuePatientDrugs should refuse a request that carries neither lines nor a prescription") + void issuePatientDrugs_shouldRefuseEmptyRequest() { + T_PatientIssue issue = patientIssue(new ArrayList<>()); + + InventoryException ex = assertThrows(InventoryException.class, () -> service.issuePatientDrugs(issue)); + assertEquals("No item found to dispense.", ex.getMessage()); + } + + @Test + @DisplayName("issuePatientDrugs should close the visit flow when a prescription is dispensed with no lines") + void issuePatientDrugs_shouldCloseFlowForEmptyPrescription() throws Exception { + T_PatientIssue issue = patientIssue(new ArrayList<>()); + issue.setPrescriptionID(9001); + when(patientIssueRepo.updateBenStatusFlowAfterPharma(101L, 5001L)).thenReturn(1); + + assertEquals(1, service.issuePatientDrugs(issue)); + verify(patientIssueRepo, never()).save(any(T_PatientIssue.class)); + } + + @Test + @DisplayName("issuePatientDrugs should report failure when the visit flow could not be closed") + void issuePatientDrugs_shouldReportFailureWhenFlowNotClosed() throws Exception { + T_PatientIssue issue = patientIssue(new ArrayList<>()); + issue.setPrescriptionID(9001); + when(patientIssueRepo.updateBenStatusFlowAfterPharma(101L, 5001L)).thenReturn(0); + + assertEquals(0, service.issuePatientDrugs(issue)); + } + + @Test + @DisplayName("issuePatientDrugs should report failure for a prescription with no beneficiary or visit") + void issuePatientDrugs_shouldReportFailureWithoutBeneficiary() throws Exception { + T_PatientIssue issue = patientIssue(new ArrayList<>()); + issue.setPrescriptionID(9001); + issue.setBenRegID(null); + + assertEquals(0, service.issuePatientDrugs(issue)); + } + + @Test + @DisplayName("issuePatientDrugs should book the exit lines, close the flow and stamp the pharmacist") + void issuePatientDrugs_shouldBookLinesAndStampPharmacist() throws Exception { + T_PatientIssue issue = patientIssue(new ArrayList<>(List.of(exitLine(601L, 6)))); + M_User pharmacist = new M_User(); + pharmacist.setUserID(42); + when(stockEntryService.getAllItemBatchForStoreID(eq(7), any(Long[].class))) + .thenReturn(rows(stockRow(601L, 40, 601L))); + when(patientIssueRepo.updateBenStatusFlowAfterPharma(101L, 5001L)).thenReturn(1); + when(userLoginRepo.getUserByUserName("pharma.user")).thenReturn(pharmacist); + + assertEquals(1, service.issuePatientDrugs(issue)); + + assertEquals(7, issue.getSyncFacilityID()); + verify(patientIssueRepo).save(issue); + verify(patientIssueRepo).updateVanSerialNo(); + verify(itemStockExitRepo).saveAll(anyList()); + verify(stockEntryService).updateStocks(anyList()); + verify(patientIssueRepo).updatePharmacistID(42L, 101L, 5001L); + } + + @Test + @DisplayName("issuePatientDrugs should still dispense when the pharmacist username cannot be resolved") + void issuePatientDrugs_shouldDispenseWithUnresolvablePharmacist() throws Exception { + T_PatientIssue issue = patientIssue(new ArrayList<>(List.of(exitLine(601L, 6)))); + issue.setCreatedBy(" "); + when(stockEntryService.getAllItemBatchForStoreID(eq(7), any(Long[].class))) + .thenReturn(rows(stockRow(601L, 40, 601L))); + when(patientIssueRepo.updateBenStatusFlowAfterPharma(101L, 5001L)).thenReturn(1); + + assertEquals(1, service.issuePatientDrugs(issue)); + verify(patientIssueRepo, never()).updatePharmacistID(anyLong(), anyLong(), anyLong()); + } + + @Test + @DisplayName("issuePatientDrugs should not stamp a pharmacist that the user store does not know") + void issuePatientDrugs_shouldNotStampUnknownPharmacist() throws Exception { + T_PatientIssue issue = patientIssue(new ArrayList<>(List.of(exitLine(601L, 6)))); + when(stockEntryService.getAllItemBatchForStoreID(eq(7), any(Long[].class))) + .thenReturn(rows(stockRow(601L, 40, 601L))); + when(patientIssueRepo.updateBenStatusFlowAfterPharma(101L, 5001L)).thenReturn(1); + when(userLoginRepo.getUserByUserName("pharma.user")).thenReturn(null); + + assertEquals(1, service.issuePatientDrugs(issue)); + verify(patientIssueRepo, never()).updatePharmacistID(anyLong(), anyLong(), anyLong()); + } + + @Test + @DisplayName("issuePatientDrugs should book nothing when a line asks for more than the batch holds") + void issuePatientDrugs_shouldBookNothingWhenStockShort() throws Exception { + T_PatientIssue issue = patientIssue(new ArrayList<>(List.of(exitLine(601L, 500)))); + when(stockEntryService.getAllItemBatchForStoreID(eq(7), any(Long[].class))) + .thenReturn(rows(stockRow(601L, 40, 601L))); + + assertEquals(0, service.issuePatientDrugs(issue)); + verify(patientIssueRepo, never()).save(any(T_PatientIssue.class)); + } + + @Test + @DisplayName("getItemStockAndValidate should stamp the store details onto each dispensable line") + void getItemStockAndValidate_shouldStampStoreDetails() { + when(stockEntryService.getAllItemBatchForStoreID(eq(7), any(Long[].class))) + .thenReturn(rows(stockRow(601L, 40, 610L))); + + List result = service.getItemStockAndValidate( + new ArrayList<>(List.of(exitLine(601L, 6))), 7, "tester", 4L, 2L); + + assertEquals(1, result.size()); + ItemStockExit line = result.get(0); + assertEquals(610L, line.getItemStockEntryID(), "the line is redirected to the batch actually holding stock"); + assertEquals(40, line.getQuantityInHand()); + assertEquals("tester", line.getCreatedBy()); + assertEquals(4L, line.getVanID()); + assertEquals(2L, line.getParkingPlaceID()); + assertEquals(7, line.getFacilityID()); + assertEquals(7, line.getSyncFacilityID()); + } + + @Test + @DisplayName("getItemStockAndValidate should ignore a stock row that matches no requested line") + void getItemStockAndValidate_shouldIgnoreUnmatchedStockRow() { + when(stockEntryService.getAllItemBatchForStoreID(eq(7), any(Long[].class))) + .thenReturn(rows(stockRow(999L, 40, 999L))); + + assertTrue(service.getItemStockAndValidate( + new ArrayList<>(List.of(exitLine(601L, 6))), 7, "tester", 4L, 2L).isEmpty()); + } + + @Test + @DisplayName("saveItemExit should tag every line with the issue type and id before saving") + void saveItemExit_shouldTagLinesWithIssue() { + List lines = new ArrayList<>(List.of(exitLine(601L, 6))); + + assertEquals(1, service.saveItemExit(lines, 88L, "T_PatientIssue")); + + assertEquals("T_PatientIssue", lines.get(0).getExitType()); + assertEquals(88L, lines.get(0).getExitTypeID()); + verify(itemStockExitRepo).saveAll(lines); + verify(itemStockExitRepo).updateVanSerialNo(); + verify(stockEntryService).updateStocks(lines); + } + + @Test + @DisplayName("storeSelfConsumption should book the consumption when every line is covered by stock") + void storeSelfConsumption_shouldBookConsumption() { + StoreSelfConsumption consumption = new StoreSelfConsumption(); + consumption.setConsumptionID(66L); + consumption.setFacilityID(7); + consumption.setCreatedBy("tester"); + consumption.setItemStockExit(new ArrayList<>(List.of(exitLine(601L, 6)))); + when(stockEntryService.getAllItemBatchForStoreID(eq(7), any(Long[].class))) + .thenReturn(rows(stockRow(601L, 40, 601L))); + + assertEquals(1, service.storeSelfConsumption(consumption)); + + assertEquals(7, consumption.getSyncFacilityID()); + verify(storeSelfConsumptionRepo).save(consumption); + verify(storeSelfConsumptionRepo).updateVanSerialNo(); + } + + @Test + @DisplayName("storeSelfConsumption should book nothing when a line asks for more than the batch holds") + void storeSelfConsumption_shouldBookNothingWhenStockShort() { + StoreSelfConsumption consumption = new StoreSelfConsumption(); + consumption.setFacilityID(7); + consumption.setItemStockExit(new ArrayList<>(List.of(exitLine(601L, 500)))); + when(stockEntryService.getAllItemBatchForStoreID(eq(7), any(Long[].class))) + .thenReturn(rows(stockRow(601L, 40, 601L))); + + assertEquals(0, service.storeSelfConsumption(consumption)); + verify(storeSelfConsumptionRepo, never()).save(any(StoreSelfConsumption.class)); + } + + @Test + @DisplayName("storeTransfer should move the stock out of the sending store and into the receiving one") + void storeTransfer_shouldMoveStockBetweenStores() { + T_StockTransfer transfer = new T_StockTransfer(); + transfer.setStockTransferID(99L); + transfer.setTransferFromFacilityID(1); + transfer.setTransferToFacilityID(2); + transfer.setCreatedBy("tester"); + transfer.setItemStockExit(new ArrayList<>(List.of(exitLine(601L, 6)))); + when(stockTransferRepo.findVanIDByFacID(2)).thenReturn(4L); + when(stockEntryService.getAllItemBatchForStoreID(eq(1), any(Long[].class))) + .thenReturn(rows(stockRow(601L, 40, 601L))); + + assertEquals(1, service.storeTransfer(transfer)); + + assertEquals(4L, transfer.getToVanID()); + assertEquals(1, transfer.getSyncFacilityID()); + verify(stockTransferRepo).save(transfer); + verify(stockTransferRepo).updateVanSerialNo(); + verify(stockEntryService).saveItemStockFromStockTransfer(anyList(), eq(99L), eq("T_StockTransfer"), + eq(1), eq(2), eq(4L)); + } + + @Test + @DisplayName("storeTransfer should move nothing when a line asks for more than the batch holds") + void storeTransfer_shouldMoveNothingWhenStockShort() { + T_StockTransfer transfer = new T_StockTransfer(); + transfer.setTransferFromFacilityID(1); + transfer.setTransferToFacilityID(2); + transfer.setItemStockExit(new ArrayList<>(List.of(exitLine(601L, 500)))); + when(stockEntryService.getAllItemBatchForStoreID(eq(1), any(Long[].class))) + .thenReturn(rows(stockRow(601L, 40, 601L))); + + assertEquals(0, service.storeTransfer(transfer)); + verify(stockTransferRepo, never()).save(any(T_StockTransfer.class)); + } + + @Test + @DisplayName("getStoreTransfer should widen the window to whole days before querying") + void getStoreTransfer_shouldWidenWindowToWholeDays() { + List found = List.of(new T_StockTransfer()); + Timestamp from = Timestamp.valueOf("2025-01-01 00:00:00"); + Timestamp to = Timestamp.valueOf("2025-01-31 23:59:00"); + when(stockTransferRepo + .findByCreatedDateBetweenAndTransferFromFacilityIDOrCreatedDateBetweenAndTransferToFacilityIDOrderByCreatedDateDesc( + from, to, 7, from, to, 7)).thenReturn(found); + + assertSame(found, service.getStoreTransfer(window(7, "2025-01-01 08:30:00", "2025-01-31 08:30:00"))); + } + + @Test + @DisplayName("getStoreTransfer should return nothing when the window is incomplete") + void getStoreTransfer_shouldReturnNothingForIncompleteWindow() { + assertTrue(service.getStoreTransfer(window(null, null, null)).isEmpty()); + assertTrue(service.getStoreTransfer(window(7, null, null)).isEmpty()); + assertTrue(service.getStoreTransfer(window(7, "2025-01-01 08:30:00", null)).isEmpty()); + } + + @Test + @DisplayName("getpatientIssue should widen the window to whole days before querying") + void getpatientIssue_shouldWidenWindowToWholeDays() { + List found = List.of(new T_PatientIssue()); + when(patientIssueRepo.findByFacilityIDAndCreatedDateBetweenOrderByCreatedDateDesc( + 7, Timestamp.valueOf("2025-01-01 00:00:00"), Timestamp.valueOf("2025-01-31 23:59:00"))) + .thenReturn(found); + + assertSame(found, service.getpatientIssue(window(7, "2025-01-01 08:30:00", "2025-01-31 08:30:00"))); + } + + @Test + @DisplayName("getpatientIssue should return nothing when the window is incomplete") + void getpatientIssue_shouldReturnNothingForIncompleteWindow() { + assertTrue(service.getpatientIssue(window(null, null, null)).isEmpty()); + assertTrue(service.getpatientIssue(window(7, null, null)).isEmpty()); + assertTrue(service.getpatientIssue(window(7, "2025-01-01 08:30:00", null)).isEmpty()); + } + + @Test + @DisplayName("getstoreSelfConsumption should widen the window to whole days before querying") + void getstoreSelfConsumption_shouldWidenWindowToWholeDays() { + List found = List.of(new StoreSelfConsumption()); + when(storeSelfConsumptionRepo.findByFacilityIDAndCreatedDateBetweenOrderByCreatedDateDesc( + 7, Timestamp.valueOf("2025-01-01 00:00:00"), Timestamp.valueOf("2025-01-31 23:59:00"))) + .thenReturn(found); + + assertSame(found, service.getstoreSelfConsumption(window(7, "2025-01-01 08:30:00", "2025-01-31 08:30:00"))); + } + + @Test + @DisplayName("getstoreSelfConsumption should return nothing when the window is incomplete") + void getstoreSelfConsumption_shouldReturnNothingForIncompleteWindow() { + assertTrue(service.getstoreSelfConsumption(window(null, null, null)).isEmpty()); + assertTrue(service.getstoreSelfConsumption(window(7, null, null)).isEmpty()); + assertTrue(service.getstoreSelfConsumption(window(7, "2025-01-01 08:30:00", null)).isEmpty()); + } + + @Test + @DisplayName("getstoreSelfConsumptionItemList should project the lines booked under that consumption") + void getstoreSelfConsumptionItemList_shouldProjectBookedLines() { + StoreSelfConsumption consumption = new StoreSelfConsumption(); + consumption.setVanSerialNo(66L); + consumption.setSyncFacilityID(7); + ItemStockEntryinput input = new ItemStockEntryinput(); + input.setConsumptionID(66L); + List projected = List.of(new ItemStockExitMap()); + List lines = List.of(exitLine(601L, 6)); + when(storeSelfConsumptionRepo.findByConsumptionID(66L)).thenReturn(consumption); + when(itemStockExitRepo.findByExitTypeIDAndSyncFacilityIDAndExitType(66L, 7, "StoreSelfConsumption")) + .thenReturn(lines); + when(itemStockExitMapper.getItemStockExitMapList(lines)).thenReturn(projected); + + assertSame(projected, service.getstoreSelfConsumptionItemList(input)); + } + + @Test + @DisplayName("getpatientIssueItemLIst should project the lines booked under that patient issue") + void getpatientIssueItemLIst_shouldProjectBookedLines() { + T_PatientIssue issue = new T_PatientIssue(); + issue.setVanSerialNo(88L); + issue.setSyncFacilityID(7); + ItemStockEntryinput input = new ItemStockEntryinput(); + input.setPatientIssueID(88L); + List projected = List.of(new ItemStockExitMap()); + List lines = List.of(exitLine(601L, 6)); + when(patientIssueRepo.findById(88L)).thenReturn(Optional.of(issue)); + when(itemStockExitRepo.findByExitTypeIDAndSyncFacilityIDAndExitType(88L, 7, "T_PatientIssue")) + .thenReturn(lines); + when(itemStockExitMapper.getItemStockExitMapList(lines)).thenReturn(projected); + + assertSame(projected, service.getpatientIssueItemLIst(input)); + } + + @Test + @DisplayName("getStoreTransferItemEntry should project the batches booked into the receiving store") + void getStoreTransferItemEntry_shouldProjectReceivedBatches() { + T_StockTransfer transfer = new T_StockTransfer(); + transfer.setVanSerialNo(99L); + transfer.setSyncFacilityID(1); + ItemStockEntryinput input = new ItemStockEntryinput(); + input.setStockTransferID(99L); + List projected = List.of(new ItemStockExitMap()); + List batches = List.of(new ItemStockEntry()); + when(stockTransferRepo.findByStockTransferID(99L)).thenReturn(transfer); + when(itemStockEntryRepo.findByEntryTypeIDAndSyncFacilityIDAndEntryType(99L, 1, "T_StockTransfer")) + .thenReturn(batches); + when(itemStockExitMapper.getItemStockEntryMapList(batches)).thenReturn(projected); + + assertSame(projected, service.getStoreTransferItemEntry(input)); + } + + @Test + @DisplayName("getPatientissueAllDetail should attach the dispensed lines to the patient issue it loads") + void getPatientissueAllDetail_shouldAttachDispensedLines() { + T_PatientIssue issue = new T_PatientIssue(); + issue.setVanSerialNo(88L); + issue.setSyncFacilityID(7); + List projected = List.of(new ItemStockExitMap()); + List lines = List.of(exitLine(601L, 6)); + when(patientIssueRepo.findById(88L)).thenReturn(Optional.of(issue)); + when(itemStockExitRepo.findByExitTypeIDAndSyncFacilityIDAndExitType(88L, 7, "T_PatientIssue")) + .thenReturn(lines); + when(itemStockExitMapper.getItemStockExitMapList(lines)).thenReturn(projected); + + assertSame(projected, service.getPatientissueAllDetail(88L).getItemStockExitMap()); + } +} diff --git a/src/test/java/com/iemr/inventory/service/stockadjustment/StockAdjustmentServiceImplTest.java b/src/test/java/com/iemr/inventory/service/stockadjustment/StockAdjustmentServiceImplTest.java new file mode 100644 index 00000000..e2154319 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/stockadjustment/StockAdjustmentServiceImplTest.java @@ -0,0 +1,339 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.stockadjustment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +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.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import com.iemr.inventory.data.stockadjustment.StockAdjustment; +import com.iemr.inventory.data.stockadjustment.StockAdjustmentDraft; +import com.iemr.inventory.data.stockadjustment.StockAdjustmentItem; +import com.iemr.inventory.data.stockadjustment.StockAdjustmentItemDraft; +import com.iemr.inventory.data.stockadjustment.StockAdjustmentItemDraftEdit; +import com.iemr.inventory.data.stockentry.ItemStockEntry; +import com.iemr.inventory.data.stockentry.ItemStockEntryinput; +import com.iemr.inventory.mapper.stockadjustment.StockAdjustmentItemDraftMapper; +import com.iemr.inventory.repo.stockadjustment.StockAdjustmentDraftRepo; +import com.iemr.inventory.repo.stockadjustment.StockAdjustmentItemDraftRepo; +import com.iemr.inventory.repo.stockadjustment.StockAdjustmentItemRepo; +import com.iemr.inventory.repo.stockadjustment.StockAdjustmentRepo; +import com.iemr.inventory.repo.stockEntry.ItemStockEntryRepo; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("StockAdjustmentServiceImpl Test Suite") +class StockAdjustmentServiceImplTest { + + @Mock + private StockAdjustmentDraftRepo stockAdjustmentDraftRepo; + @Mock + private StockAdjustmentItemDraftRepo stockAdjustmentItemDraftRepo; + @Mock + private StockAdjustmentRepo stockAdjustmentRepo; + @Mock + private StockAdjustmentItemRepo stockAdjustmentItemRepo; + @Mock + private ItemStockEntryRepo itemStockEntryRepo; + @Mock + private StockAdjustmentItemDraftMapper stockAdjustmentItemDraftMapper; + + @InjectMocks + private StockAdjustmentServiceImpl service; + + private static StockAdjustmentItemDraft itemDraft(Long mapID) { + StockAdjustmentItemDraft draftItem = new StockAdjustmentItemDraft(); + draftItem.setSADraftItemMapID(mapID); + draftItem.setCreatedBy("tester"); + return draftItem; + } + + private static StockAdjustmentDraft draft(Long draftID, StockAdjustmentItemDraft... items) { + StockAdjustmentDraft draft = new StockAdjustmentDraft(); + draft.setStockAdjustmentDraftID(draftID); + draft.setFacilityID(7); + draft.setStockAdjustmentItemDraft(new ArrayList<>(List.of(items))); + return draft; + } + + private static StockAdjustmentItem adjustmentItem(Long stockEntryID, boolean added, Integer quantity) { + StockAdjustmentItem item = new StockAdjustmentItem(); + item.setItemStockEntryID(stockEntryID); + item.setIsAdded(added); + item.setAdjustedQuantity(quantity); + return item; + } + + private static ItemStockEntry stockEntry(Integer id, Integer inHand) { + ItemStockEntry entry = new ItemStockEntry(); + entry.setItemStockEntryID(id); + entry.setQuantityInHand(inHand); + return entry; + } + + private static ItemStockEntryinput window(Integer facilityID, String from, String to) { + ItemStockEntryinput input = new ItemStockEntryinput(); + input.setFacilityID(facilityID); + input.setFromDate(from == null ? null : Timestamp.valueOf(from)); + input.setToDate(to == null ? null : Timestamp.valueOf(to)); + return input; + } + + @Test + @DisplayName("saveDraft should insert a brand new draft and attach the saved item rows to it") + void saveDraft_shouldInsertNewDraft() { + StockAdjustmentDraft request = draft(null, itemDraft(null)); + StockAdjustmentDraft persisted = draft(55L); + when(stockAdjustmentDraftRepo.save(request)).thenReturn(persisted); + when(stockAdjustmentItemDraftRepo.saveAll(anyList())).thenAnswer(inv -> inv.getArgument(0)); + + StockAdjustmentDraft result = service.saveDraft(request); + + assertSame(persisted, result); + assertEquals(Boolean.FALSE, request.getIsCompleted()); + assertEquals(1, result.getStockAdjustmentItemDraft().size()); + assertEquals(55L, result.getStockAdjustmentItemDraft().get(0).getStockAdjustmentDraftID()); + verify(stockAdjustmentDraftRepo, never()).updateStock(anyLong(), any(), any(), any(), any()); + } + + @Test + @DisplayName("saveDraft should treat a zero draft id as a brand new draft") + void saveDraft_shouldTreatZeroIdAsNewDraft() { + StockAdjustmentDraft request = draft(0L, itemDraft(null)); + when(stockAdjustmentDraftRepo.save(request)).thenReturn(draft(55L)); + when(stockAdjustmentItemDraftRepo.saveAll(anyList())).thenAnswer(inv -> inv.getArgument(0)); + + service.saveDraft(request); + + verify(stockAdjustmentDraftRepo).save(request); + } + + @Test + @DisplayName("saveDraft should update an existing draft and soft-delete its previous item rows") + void saveDraft_shouldUpdateExistingDraft() { + StockAdjustmentDraft request = draft(55L, itemDraft(null)); + request.setDraftName("a name"); + request.setDraftDesc("a description"); + request.setRefNo("REF-1"); + request.setCreatedBy("tester"); + StockAdjustmentDraft persisted = draft(55L); + when(stockAdjustmentDraftRepo.findById(55L)).thenReturn(java.util.Optional.of(persisted)); + when(stockAdjustmentItemDraftRepo.saveAll(anyList())).thenAnswer(inv -> inv.getArgument(0)); + + service.saveDraft(request); + + verify(stockAdjustmentDraftRepo).updateStock(55L, "a description", "a name", "REF-1", "tester"); + verify(stockAdjustmentItemDraftRepo).updateDeleted(55L); + verify(stockAdjustmentDraftRepo, never()).save(request); + } + + @Test + @DisplayName("saveDraft should revive an existing item row rather than replacing its audit trail") + void saveDraft_shouldReviveExistingItemRow() { + StockAdjustmentItemDraft stored = itemDraft(9L); + stored.setCreatedDate(Timestamp.valueOf("2025-01-31 10:15:30")); + stored.setProcessed('N'); + + StockAdjustmentDraft request = draft(null, itemDraft(9L)); + when(stockAdjustmentDraftRepo.save(request)).thenReturn(draft(55L)); + when(stockAdjustmentItemDraftRepo.findById(9L)).thenReturn(java.util.Optional.of(stored)); + when(stockAdjustmentItemDraftRepo.saveAll(anyList())).thenAnswer(inv -> inv.getArgument(0)); + + StockAdjustmentItemDraft saved = service.saveDraft(request).getStockAdjustmentItemDraft().get(0); + + assertEquals(Boolean.FALSE, saved.getDeleted()); + assertEquals("tester", saved.getModifiedBy()); + assertEquals(Timestamp.valueOf("2025-01-31 10:15:30"), saved.getCreatedDate()); + assertEquals('N', saved.getProcessed()); + } + + @Test + @DisplayName("getStockAjustmentDraftTransaction should widen the window to whole days before querying") + void getStockAjustmentDraftTransaction_shouldWidenWindowToWholeDays() { + ItemStockEntryinput input = window(7, "2025-01-01 08:30:00", "2025-01-31 08:30:00"); + List found = List.of(draft(55L)); + when(stockAdjustmentDraftRepo.findByIsCompletedAndFacilityIDAndCreatedDateBetweenOrderByCreatedDateDesc( + false, 7, Timestamp.valueOf("2025-01-01 00:00:00"), Timestamp.valueOf("2025-01-31 23:59:00"))) + .thenReturn(found); + + assertSame(found, service.getStockAjustmentDraftTransaction(input)); + } + + @Test + @DisplayName("getStockAjustmentDraftTransaction should return nothing when the facility is missing") + void getStockAjustmentDraftTransaction_shouldReturnNothingWithoutFacility() { + assertTrue(service.getStockAjustmentDraftTransaction( + window(null, "2025-01-01 08:30:00", "2025-01-31 08:30:00")).isEmpty()); + } + + @Test + @DisplayName("getStockAjustmentDraftTransaction should return nothing when the from date is missing") + void getStockAjustmentDraftTransaction_shouldReturnNothingWithoutFromDate() { + assertTrue(service.getStockAjustmentDraftTransaction(window(7, null, "2025-01-31 08:30:00")).isEmpty()); + } + + @Test + @DisplayName("getStockAjustmentDraftTransaction should return nothing when the to date is missing") + void getStockAjustmentDraftTransaction_shouldReturnNothingWithoutToDate() { + assertTrue(service.getStockAjustmentDraftTransaction(window(7, "2025-01-01 08:30:00", null)).isEmpty()); + } + + @Test + @DisplayName("getforeditStockAjustmentDraftTransaction should swap the item rows for their edit projections") + void getforeditStockAjustmentDraftTransaction_shouldSwapItemRowsForEditProjections() { + StockAdjustmentDraft stored = draft(55L, itemDraft(9L)); + List projections = List.of(new StockAdjustmentItemDraftEdit()); + when(stockAdjustmentDraftRepo.getforedit(55L)).thenReturn(stored); + when(stockAdjustmentItemDraftMapper.getStockAdjustmentItemDraftEditList(anyList())).thenReturn(projections); + + StockAdjustmentDraft result = service.getforeditStockAjustmentDraftTransaction(55L); + + assertSame(projections, result.getStockAdjustmentItemDraftEdit()); + assertNull(result.getStockAdjustmentItemDraft()); + } + + @Test + @DisplayName("savetransaction should add stock for an addition and subtract it for an issue") + void savetransaction_shouldAddAndSubtractStock() throws Exception { + StockAdjustment adjustment = new StockAdjustment(); + adjustment.setFacilityID(7); + adjustment.setStockAdjustmentID(88L); + adjustment.setStockAdjustmentItem(new ArrayList<>(List.of( + adjustmentItem(101L, true, 5), adjustmentItem(102L, false, 3)))); + when(itemStockEntryRepo.findByItemStockEntryIDIn(anyList())) + .thenReturn(List.of(stockEntry(101, 50), stockEntry(102, 40))); + + StockAdjustment result = service.savetransaction(adjustment); + + assertEquals(7, result.getSyncFacilityID()); + assertEquals(2, result.getStockAdjustmentItem().size()); + verify(itemStockEntryRepo).addStock(101L, 5); + verify(itemStockEntryRepo).subtractStock(102L, 3); + verify(stockAdjustmentRepo).updateVanSerialNo(); + verify(stockAdjustmentItemRepo).updateVanSerialNo(); + } + + @Test + @DisplayName("savetransaction does NOT currently stop an issue larger than the batch holds") + void savetransaction_doesNotStopIssueBeyondAvailableStock() throws Exception { + // The over-issue guard looks the batch up in a map keyed by ItemStockEntry.getItemStockEntryID(), + // which is an Integer, using StockAdjustmentItem.getItemStockEntryID(), which is a Long. Those + // keys never match, so the lookup always yields null and the guard never fires. This test pins + // the behaviour as it stands today; fixing the key mismatch should make it start throwing. + StockAdjustment adjustment = new StockAdjustment(); + adjustment.setFacilityID(7); + adjustment.setStockAdjustmentID(88L); + adjustment.setStockAdjustmentItem(new ArrayList<>(List.of(adjustmentItem(101L, false, 500)))); + when(itemStockEntryRepo.findByItemStockEntryIDIn(anyList())).thenReturn(List.of(stockEntry(101, 50))); + + service.savetransaction(adjustment); + + verify(itemStockEntryRepo).subtractStock(101L, 500); + } + + @Test + @DisplayName("savetransaction should close the originating draft once the adjustment is posted") + void savetransaction_shouldCloseOriginatingDraft() throws Exception { + StockAdjustment adjustment = new StockAdjustment(); + adjustment.setFacilityID(7); + adjustment.setStockAdjustmentID(88L); + adjustment.setStockAdjustmentDraftID(55L); + adjustment.setStockAdjustmentItem(new ArrayList<>(List.of(adjustmentItem(101L, true, 5)))); + when(itemStockEntryRepo.findByItemStockEntryIDIn(anyList())).thenReturn(List.of(stockEntry(101, 50))); + + service.savetransaction(adjustment); + + verify(stockAdjustmentDraftRepo).updatecompleted(55L, true); + } + + @Test + @DisplayName("savetransaction should not close any draft when the adjustment did not come from one") + void savetransaction_shouldNotCloseDraftWhenNoneSupplied() throws Exception { + StockAdjustment adjustment = new StockAdjustment(); + adjustment.setFacilityID(7); + adjustment.setStockAdjustmentID(88L); + adjustment.setStockAdjustmentItem(new ArrayList<>(List.of(adjustmentItem(101L, true, 5)))); + when(itemStockEntryRepo.findByItemStockEntryIDIn(anyList())).thenReturn(List.of(stockEntry(101, 50))); + + service.savetransaction(adjustment); + + verify(stockAdjustmentDraftRepo, never()).updatecompleted(anyLong(), any(Boolean.class)); + } + + @Test + @DisplayName("getforeditStockAjustmentTransaction should swap the item rows for their edit projections") + void getforeditStockAjustmentTransaction_shouldSwapItemRowsForEditProjections() { + StockAdjustment stored = new StockAdjustment(); + stored.setVanSerialNo(88L); + stored.setSyncFacilityID(7); + List items = List.of(adjustmentItem(101L, true, 5)); + List projections = List.of(new StockAdjustmentItemDraftEdit()); + when(stockAdjustmentRepo.findById(88L)).thenReturn(java.util.Optional.of(stored)); + when(stockAdjustmentItemRepo.findByStockAdjustmentIDAndSyncFacilityID(88L, 7)).thenReturn(items); + when(stockAdjustmentItemDraftMapper.getStockAdjustmentItemEditList(items)).thenReturn(projections); + + StockAdjustment result = service.getforeditStockAjustmentTransaction(88L); + + assertSame(projections, result.getStockAdjustmentItemDraftEdit()); + assertNull(result.getStockAdjustmentItem()); + } + + @Test + @DisplayName("getStockAjustmentTransaction should widen the window to whole days before querying") + void getStockAjustmentTransaction_shouldWidenWindowToWholeDays() { + ItemStockEntryinput input = window(7, "2025-01-01 08:30:00", "2025-01-31 08:30:00"); + List found = List.of(new StockAdjustment()); + when(stockAdjustmentRepo.findByFacilityIDAndCreatedDateBetweenOrderByCreatedDateDesc( + 7, Timestamp.valueOf("2025-01-01 00:00:00"), Timestamp.valueOf("2025-01-31 23:59:00"))) + .thenReturn(found); + + assertSame(found, service.getStockAjustmentTransaction(input)); + } + + @Test + @DisplayName("getStockAjustmentTransaction should return nothing when the window is incomplete") + void getStockAjustmentTransaction_shouldReturnNothingForIncompleteWindow() { + assertTrue(service.getStockAjustmentTransaction(window(null, null, null)).isEmpty()); + assertTrue(service.getStockAjustmentTransaction(window(7, null, null)).isEmpty()); + assertTrue(service.getStockAjustmentTransaction(window(7, "2025-01-01 08:30:00", null)).isEmpty()); + } +} diff --git a/src/test/java/com/iemr/inventory/service/store/StoreServiceImplTest.java b/src/test/java/com/iemr/inventory/service/store/StoreServiceImplTest.java new file mode 100644 index 00000000..6f36a83f --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/store/StoreServiceImplTest.java @@ -0,0 +1,256 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.store; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +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 com.iemr.inventory.data.store.M_Facility; +import com.iemr.inventory.data.store.M_Van; +import com.iemr.inventory.repository.store.MainStoreRepo; +import com.iemr.inventory.repository.store.VanMasterRepository; +import com.iemr.inventory.utils.exception.IEMRException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("StoreServiceImpl Test Suite") +class StoreServiceImplTest { + + @Mock + private MainStoreRepo mainStoreRepo; + @Mock + private VanMasterRepository vanMasterRepository; + + @InjectMocks + private StoreServiceImpl service; + + private static M_Facility facility(Integer id) { + M_Facility facility = new M_Facility(); + facility.setFacilityID(id); + return facility; + } + + @Test + @DisplayName("createMainStore should persist the store through the main store repository") + void createMainStore_shouldPersist() { + M_Facility store = facility(7); + when(mainStoreRepo.save(store)).thenReturn(store); + + assertSame(store, service.createMainStore(store)); + } + + @Test + @DisplayName("getMainStore should unwrap the store the repository found") + void getMainStore_shouldUnwrapFoundStore() { + M_Facility store = facility(7); + when(mainStoreRepo.findById(7)).thenReturn(Optional.of(store)); + + assertSame(store, service.getMainStore(7)); + } + + @Test + @DisplayName("getMainStore should throw when no store carries that id") + void getMainStore_shouldThrowWhenStoreMissing() { + when(mainStoreRepo.findById(99)).thenReturn(Optional.empty()); + + assertThrows(NoSuchElementException.class, () -> service.getMainStore(99)); + } + + @Test + @DisplayName("getAllMainStore should return the stores of the provider service map") + void getAllMainStore_shouldReturnStoresOfProviderServiceMap() { + List stores = List.of(facility(7)); + when(mainStoreRepo.findByProviderServiceMapID(3)).thenReturn(stores); + + assertSame(stores, service.getAllMainStore(3)); + } + + @Test + @DisplayName("addAllMainStore should save the whole batch in one call") + void addAllMainStore_shouldSaveBatch() { + List batch = List.of(facility(7), facility(8)); + when(mainStoreRepo.saveAll(batch)).thenReturn(batch); + + assertSame(batch, service.addAllMainStore(batch)); + } + + @Test + @DisplayName("getMainFacility should return the main facilities of the provider service map") + void getMainFacility_shouldReturnMainFacilities() { + ArrayList found = new ArrayList<>(List.of(facility(7))); + when(mainStoreRepo.getAllMainFacility(3, true)).thenReturn(found); + + assertSame(found, service.getMainFacility(3, true)); + } + + @Test + @DisplayName("getMainFacility should narrow to one parent facility when a parent id is supplied") + void getMainFacility_shouldNarrowToParentFacility() { + ArrayList found = new ArrayList<>(List.of(facility(7))); + when(mainStoreRepo.getAllMainFacility(3, true, 5)).thenReturn(found); + + assertSame(found, service.getMainFacility(3, true, 5)); + } + + @Test + @DisplayName("getChildFacility should return the sub-stores of the parent facility") + void getChildFacility_shouldReturnSubStores() { + ArrayList found = new ArrayList<>(List.of(facility(8))); + when(mainStoreRepo.getChildFacility(3, 7)).thenReturn(found); + + assertSame(found, service.getChildFacility(3, 7)); + } + + @Test + @DisplayName("deleteStore should deactivate a store that has no active children left") + void deleteStore_shouldDeactivateStoreWithoutActiveChildren() throws Exception { + M_Facility stored = facility(7); + M_Facility request = facility(7); + request.setDeleted(true); + when(mainStoreRepo.findById(7)).thenReturn(Optional.of(stored)); + when(mainStoreRepo.findByMainFacilityIDAndDeleted(7, false)).thenReturn(new ArrayList<>()); + when(mainStoreRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.deleteStore(request)); + assertEquals(Boolean.TRUE, stored.getDeleted()); + } + + @Test + @DisplayName("deleteStore should refuse to deactivate a store that still has active children") + void deleteStore_shouldRefuseWhenChildrenStillActive() { + M_Facility stored = facility(7); + M_Facility request = facility(7); + request.setDeleted(true); + when(mainStoreRepo.findById(7)).thenReturn(Optional.of(stored)); + when(mainStoreRepo.findByMainFacilityIDAndDeleted(7, false)).thenReturn(new ArrayList<>(List.of(facility(8)))); + + IEMRException ex = assertThrows(IEMRException.class, () -> service.deleteStore(request)); + assertEquals("Child Stores are still active", ex.getMessage()); + verify(mainStoreRepo, never()).save(any(M_Facility.class)); + } + + @Test + @DisplayName("deleteStore should reactivate a sub-store whose parent is already active") + void deleteStore_shouldReactivateSubStoreWithActiveParent() throws Exception { + M_Facility stored = facility(8); + stored.setMainFacilityID(7); + M_Facility request = facility(8); + request.setDeleted(false); + when(mainStoreRepo.findById(8)).thenReturn(Optional.of(stored)); + when(mainStoreRepo.findByFacilityIDAndDeleted(7, false)).thenReturn(facility(7)); + when(mainStoreRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.deleteStore(request)); + assertEquals(Boolean.FALSE, stored.getDeleted()); + } + + @Test + @DisplayName("deleteStore should refuse to reactivate a sub-store whose parent is still inactive") + void deleteStore_shouldRefuseWhenParentStillInactive() { + M_Facility stored = facility(8); + stored.setMainFacilityID(7); + M_Facility request = facility(8); + request.setDeleted(false); + when(mainStoreRepo.findById(8)).thenReturn(Optional.of(stored)); + when(mainStoreRepo.findByFacilityIDAndDeleted(7, false)).thenReturn(null); + + IEMRException ex = assertThrows(IEMRException.class, () -> service.deleteStore(request)); + assertEquals("Parent Stores are still inactive", ex.getMessage()); + } + + @Test + @DisplayName("deleteStore should reactivate a top-level store with no parent to check") + void deleteStore_shouldReactivateTopLevelStore() throws Exception { + M_Facility stored = facility(7); + M_Facility request = facility(7); + request.setDeleted(false); + when(mainStoreRepo.findById(7)).thenReturn(Optional.of(stored)); + when(mainStoreRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.deleteStore(request)); + assertEquals(Boolean.FALSE, stored.getDeleted()); + } + + @Test + @DisplayName("deleteStore should refuse a request that carries no deleted flag") + void deleteStore_shouldRefuseRequestWithoutDeletedFlag() { + when(mainStoreRepo.findById(7)).thenReturn(Optional.of(facility(7))); + + IEMRException ex = assertThrows(IEMRException.class, () -> service.deleteStore(facility(7))); + assertEquals("No store available", ex.getMessage()); + } + + @Test + @DisplayName("getAllActiveStore should filter on the provider service map and deleted flag of the probe") + void getAllActiveStore_shouldFilterOnProbe() { + M_Facility probe = new M_Facility(); + probe.setProviderServiceMapID(3); + probe.setDeleted(false); + List active = List.of(facility(7)); + when(mainStoreRepo.findByProviderServiceMapIDAndDeleted(3, false)).thenReturn(active); + + assertSame(active, service.getAllActiveStore(probe)); + } + + @Test + @DisplayName("getStoreByID should return only a store that is not deleted") + void getStoreByID_shouldReturnLiveStore() { + M_Facility store = facility(7); + when(mainStoreRepo.findByFacilityIDAndDeleted(7, false)).thenReturn(store); + + assertSame(store, service.getStoreByID(7)); + } + + @Test + @DisplayName("getVanByStoreID should return only a van that is not deleted") + void getVanByStoreID_shouldReturnLiveVan() { + M_Van van = new M_Van(); + when(vanMasterRepository.findOneByFacilityIDAndDeleted(7, false)).thenReturn(van); + + assertSame(van, service.getVanByStoreID(7)); + } + + @Test + @DisplayName("getVanByStoreID should return null when the store has no van attached") + void getVanByStoreID_shouldReturnNullWhenNoVan() { + when(vanMasterRepository.findOneByFacilityIDAndDeleted(7, false)).thenReturn(null); + + assertTrue(service.getVanByStoreID(7) == null); + } +} diff --git a/src/test/java/com/iemr/inventory/service/supplier/SupplierServiceImplTest.java b/src/test/java/com/iemr/inventory/service/supplier/SupplierServiceImplTest.java new file mode 100644 index 00000000..074adf3f --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/supplier/SupplierServiceImplTest.java @@ -0,0 +1,135 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.supplier; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +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 com.iemr.inventory.data.supplier.M_Supplier; +import com.iemr.inventory.data.supplier.M_Supplieraddress; +import com.iemr.inventory.repo.supplier.M_SupplieraddressRepo; +import com.iemr.inventory.repo.supplier.SupplierRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("SupplierServiceImpl Test Suite") +class SupplierServiceImplTest { + + @Mock + private SupplierRepo supplierRepo; + @Mock + private M_SupplieraddressRepo m_SupplieraddressRepo; + + @InjectMocks + private SupplierServiceImpl service; + + private static M_Supplier supplier(Integer id) { + M_Supplier supplier = new M_Supplier(); + supplier.setSupplierID(id); + return supplier; + } + + @Test + @DisplayName("createSupplier should return the rows the repository saved") + void createSupplier_shouldReturnSavedRows() { + List input = List.of(supplier(1), supplier(2)); + ArrayList saved = new ArrayList<>(input); + when(supplierRepo.saveAll(input)).thenReturn(saved); + + assertSame(saved, service.createSupplier(input)); + } + + @Test + @DisplayName("createSupplier should return null when the repository saved nothing") + void createSupplier_shouldReturnNullWhenNothingSaved() { + List input = List.of(); + when(supplierRepo.saveAll(input)).thenReturn(new ArrayList()); + + assertNull(service.createSupplier(input)); + } + + @Test + @DisplayName("getSupplier should hand back the rows found for the provider service map") + void getSupplier_shouldReturnRowsForProviderServiceMap() { + ArrayList found = new ArrayList<>(List.of(supplier(1))); + when(supplierRepo.getSupplierData(3)).thenReturn(found); + + assertSame(found, service.getSupplier(3)); + } + + @Test + @DisplayName("getSupplier should return null when the provider service map has no suppliers") + void getSupplier_shouldReturnNullWhenNoRows() { + when(supplierRepo.getSupplierData(3)).thenReturn(new ArrayList()); + + assertNull(service.getSupplier(3)); + } + + @Test + @DisplayName("editSupplier should hand back the supplier the repository looked up by id") + void editSupplier_shouldReturnRowById() { + M_Supplier found = supplier(1); + when(supplierRepo.geteditedData(1)).thenReturn(found); + + assertSame(found, service.editSupplier(1)); + } + + @Test + @DisplayName("editSupplier should hand back null when no supplier carries that id") + void editSupplier_shouldReturnNullWhenRowMissing() { + when(supplierRepo.geteditedData(99)).thenReturn(null); + + assertNull(service.editSupplier(99)); + } + + @Test + @DisplayName("saveEditedData should persist the edited supplier and return what the repository stored") + void saveEditedData_shouldPersistEditedRow() { + M_Supplier edited = supplier(1); + when(supplierRepo.save(edited)).thenReturn(edited); + + assertSame(edited, service.saveEditedData(edited)); + verify(supplierRepo).save(edited); + } + + @Test + @DisplayName("createAddress should persist the supplier addresses through the address repository") + void createAddress_shouldPersistAddresses() { + List input = List.of(new M_Supplieraddress()); + ArrayList saved = new ArrayList<>(input); + when(m_SupplieraddressRepo.saveAll(input)).thenReturn(saved); + + assertSame(saved, service.createAddress(input)); + verify(m_SupplieraddressRepo).saveAll(input); + } +} diff --git a/src/test/java/com/iemr/inventory/service/uom/UomServiceImplTest.java b/src/test/java/com/iemr/inventory/service/uom/UomServiceImplTest.java new file mode 100644 index 00000000..ff687c35 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/uom/UomServiceImplTest.java @@ -0,0 +1,121 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.uom; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +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 com.iemr.inventory.data.uom.M_Uom; +import com.iemr.inventory.repo.uom.UomRepo; + +@ExtendWith(MockitoExtension.class) +@DisplayName("UomServiceImpl Test Suite") +class UomServiceImplTest { + + @Mock + private UomRepo uomRepo; + + @InjectMocks + private UomServiceImpl service; + + private static M_Uom row(Integer id) { + M_Uom row = new M_Uom(); + row.setuOMID(id); + return row; + } + + @Test + @DisplayName("createDrugtypeData should return the rows the repository saved") + void createDrugtypeData_shouldReturnSavedRows() { + List input = List.of(row(1), row(2)); + ArrayList saved = new ArrayList<>(input); + when(uomRepo.saveAll(input)).thenReturn(saved); + + assertSame(saved, service.createDrugtypeData(input)); + } + + @Test + @DisplayName("createDrugtypeData should return null when the repository saved nothing") + void createDrugtypeData_shouldReturnNullWhenNothingSaved() { + List input = List.of(); + when(uomRepo.saveAll(input)).thenReturn(new ArrayList()); + + assertNull(service.createDrugtypeData(input)); + } + + @Test + @DisplayName("createDrugtypeData should hand back the rows the repository found for the provider service map") + void createDrugtypeData_shouldReturnRowsForProviderServiceMap() { + ArrayList found = new ArrayList<>(List.of(row(1))); + when(uomRepo.getUom(3)).thenReturn(found); + + assertSame(found, service.createDrugtypeData(3)); + } + + @Test + @DisplayName("createDrugtypeData should return null when the provider service map has no rows") + void createDrugtypeData_shouldReturnNullWhenNoRows() { + when(uomRepo.getUom(3)).thenReturn(new ArrayList()); + + assertNull(service.createDrugtypeData(3)); + } + + @Test + @DisplayName("editDrugtypeData should hand back the row the repository looked up by id") + void editDrugtypeData_shouldReturnRowById() { + M_Uom found = row(1); + when(uomRepo.geteditedData(1)).thenReturn(found); + + assertSame(found, service.editDrugtypeData(1)); + } + + @Test + @DisplayName("editDrugtypeData should hand back null when no row carries that id") + void editDrugtypeData_shouldReturnNullWhenRowMissing() { + when(uomRepo.geteditedData(99)).thenReturn(null); + + assertNull(service.editDrugtypeData(99)); + } + + @Test + @DisplayName("saveeditedData should persist the edited row and return what the repository stored") + void saveeditedData_shouldPersistEditedRow() { + M_Uom edited = row(1); + when(uomRepo.save(edited)).thenReturn(edited); + + assertEquals(edited, service.saveeditedData(edited)); + verify(uomRepo).save(edited); + } +} diff --git a/src/test/java/com/iemr/inventory/service/visit/VisitServiceImplTest.java b/src/test/java/com/iemr/inventory/service/visit/VisitServiceImplTest.java new file mode 100644 index 00000000..93ac0ec6 --- /dev/null +++ b/src/test/java/com/iemr/inventory/service/visit/VisitServiceImplTest.java @@ -0,0 +1,244 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.service.visit; + +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.eq; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +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.MockedConstruction; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import com.iemr.inventory.data.visit.BeneficiaryFlowStatus; +import com.iemr.inventory.data.visit.BenVisitDetail; +import com.iemr.inventory.data.visit.BeneficiaryModel; +import com.iemr.inventory.repo.visit.BeneficiaryFlowStatusRepo; +import com.iemr.inventory.repo.visit.VisitRepo; +import com.iemr.inventory.utils.CookieUtil; +import com.iemr.inventory.utils.exception.IEMRException; +import com.iemr.inventory.utils.exception.InventoryException; + +import jakarta.servlet.http.Cookie; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VisitServiceImpl Test Suite") +class VisitServiceImplTest { + + private static final String AUTH = "test-session-key"; + private static final String LOOKUP_URL = "http://common-api/searchUserById"; + private static final String SEARCH_URL = "http://common-api/searchBeneficiary"; + + private static final String ONE_BENEFICIARY = + "{\"statusCode\":200,\"data\":[{\"beneficiaryRegID\":77,\"beneficiaryID\":\"12345\"}]}"; + + @Mock + private VisitRepo visitRepo; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + @Mock + private CookieUtil cookieUtil; + + @InjectMocks + private VisitServiceImpl service; + + @BeforeEach + @DisplayName("Bind a request carrying a Jwttoken cookie and point the service at the stub URLs") + void setUp() { + ReflectionTestUtils.setField(service, "commonApiUrlSearchUserById", LOOKUP_URL); + ReflectionTestUtils.setField(service, "commonApiUrlSearchBeneficiary", SEARCH_URL); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setCookies(new Cookie("Jwttoken", "jwt-value")); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + } + + @AfterEach + @DisplayName("Unbind the request so it cannot leak into the next test") + void tearDown() { + RequestContextHolder.resetRequestAttributes(); + } + + /** Stubs every RestTemplate the service builds internally with a single canned response. */ + private MockedConstruction restTemplateReturning(ResponseEntity response) { + return mockConstruction(RestTemplate.class, (mock, context) -> + when(mock.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(response)); + } + + @Test + @DisplayName("getVisitDetail should attach the visit and flow rows to the beneficiary the lookup returned") + void getVisitDetail_shouldAttachVisitAndFlowRows() throws Exception { + List visits = List.of(new BenVisitDetail()); + List flow = List.of(new BeneficiaryFlowStatus()); + when(visitRepo.findBybeneficiaryRegIDAndProviderServiceMapID(77L, 3)).thenReturn(visits); + when(beneficiaryFlowStatusRepo + .findByBeneficiaryRegIDAndProviderServiceMapIdAndDoctorFlagInAndBenVisitIDNotNull(eq(77L), eq(3), + any(Short[].class))) + .thenReturn(flow); + + try (MockedConstruction ignored = + restTemplateReturning(new ResponseEntity<>(ONE_BENEFICIARY, HttpStatus.OK))) { + + BeneficiaryModel result = service.getVisitDetail("12345", 3, AUTH); + + assertEquals(77L, result.getBeneficiaryRegID()); + assertEquals(visits, result.getBenVisitDetail()); + assertEquals(flow, result.getBeneficiaryFlowStatus()); + } + } + + @Test + @DisplayName("getVisitDetail should reject a beneficiary id the lookup service does not know") + void getVisitDetail_shouldRejectUnknownBeneficiary() { + try (MockedConstruction ignored = restTemplateReturning( + new ResponseEntity<>("{\"statusCode\":200,\"data\":[]}", HttpStatus.OK))) { + + InventoryException ex = assertThrows(InventoryException.class, + () -> service.getVisitDetail("12345", 3, AUTH)); + assertEquals("Invalid Beneficiary ID", ex.getMessage()); + } + } + + @Test + @DisplayName("getBeneficiaryListByIDs should map every beneficiary the lookup service returned") + void getBeneficiaryListByIDs_shouldMapEveryBeneficiary() throws Exception { + String body = "{\"statusCode\":200,\"data\":[{\"beneficiaryRegID\":77},{\"beneficiaryRegID\":78}]}"; + + try (MockedConstruction ignored = + restTemplateReturning(new ResponseEntity<>(body, HttpStatus.OK))) { + + List result = service.getBeneficiaryListByIDs("12345", AUTH); + + assertEquals(2, result.size()); + assertEquals(78L, result.get(1).getBeneficiaryRegID()); + } + } + + @Test + @DisplayName("getBeneficiaryListByIDs should work without an Authorization header") + void getBeneficiaryListByIDs_shouldWorkWithoutAuthorizationHeader() throws Exception { + try (MockedConstruction ignored = + restTemplateReturning(new ResponseEntity<>(ONE_BENEFICIARY, HttpStatus.OK))) { + + assertEquals(1, service.getBeneficiaryListByIDs("12345", null).size()); + } + } + + @Test + @DisplayName("getBeneficiaryListByIDs should reject a non-OK response from the lookup service") + void getBeneficiaryListByIDs_shouldRejectNonOkResponse() { + try (MockedConstruction ignored = restTemplateReturning( + new ResponseEntity<>(ONE_BENEFICIARY, HttpStatus.INTERNAL_SERVER_ERROR))) { + + InventoryException ex = assertThrows(InventoryException.class, + () -> service.getBeneficiaryListByIDs("12345", AUTH)); + assertTrue(ex.getMessage().contains("No response or invalid status")); + } + } + + @Test + @DisplayName("getBeneficiaryListByIDs should reject a body whose own status code is not 200") + void getBeneficiaryListByIDs_shouldRejectFailingBodyStatus() { + try (MockedConstruction ignored = restTemplateReturning( + new ResponseEntity<>("{\"statusCode\":5002,\"data\":[]}", HttpStatus.OK))) { + + InventoryException ex = assertThrows(InventoryException.class, + () -> service.getBeneficiaryListByIDs("12345", AUTH)); + assertEquals("Invalid BeneficiaryRegID", ex.getMessage()); + } + } + + @Test + @DisplayName("getVisitFromAdvanceSearch should map every beneficiary the search service returned") + void getVisitFromAdvanceSearch_shouldMapEveryBeneficiary() throws Exception { + String body = "{\"statusCode\":200,\"data\":[{\"beneficiaryRegID\":77},{\"beneficiaryRegID\":78}]}"; + + try (MockedConstruction ignored = + restTemplateReturning(new ResponseEntity<>(body, HttpStatus.OK))) { + + List result = service.getVisitFromAdvanceSearch("12345", AUTH); + + assertEquals(2, result.size()); + } + } + + @Test + @DisplayName("getVisitFromAdvanceSearch should reject a non-OK response from the search service") + void getVisitFromAdvanceSearch_shouldRejectNonOkResponse() { + try (MockedConstruction ignored = restTemplateReturning( + new ResponseEntity<>(ONE_BENEFICIARY, HttpStatus.BAD_GATEWAY))) { + + IEMRException ex = assertThrows(IEMRException.class, + () -> service.getVisitFromAdvanceSearch("12345", AUTH)); + assertTrue(ex.getMessage().contains("No response or invalid status")); + } + } + + @Test + @DisplayName("getVisitFromAdvanceSearch should surface the user-id failure the search service reported") + void getVisitFromAdvanceSearch_shouldSurfaceUserIdFailure() { + String body = "{\"statusCode\":5002,\"errorMessage\":\"Invalid session\",\"data\":[]}"; + + try (MockedConstruction ignored = + restTemplateReturning(new ResponseEntity<>(body, HttpStatus.OK))) { + + IEMRException ex = assertThrows(IEMRException.class, + () -> service.getVisitFromAdvanceSearch("12345", AUTH)); + assertEquals("Invalid session", ex.getMessage()); + } + } + + @Test + @DisplayName("getVisitFromAdvanceSearch should work without an Authorization header") + void getVisitFromAdvanceSearch_shouldWorkWithoutAuthorizationHeader() throws Exception { + try (MockedConstruction ignored = + restTemplateReturning(new ResponseEntity<>(ONE_BENEFICIARY, HttpStatus.OK))) { + + assertEquals(1, service.getVisitFromAdvanceSearch("12345", null).size()); + } + } +} diff --git a/src/test/java/com/iemr/inventory/support/BeanAccessorHarness.java b/src/test/java/com/iemr/inventory/support/BeanAccessorHarness.java new file mode 100644 index 00000000..e9234827 --- /dev/null +++ b/src/test/java/com/iemr/inventory/support/BeanAccessorHarness.java @@ -0,0 +1,454 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.support; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.RegexPatternTypeFilter; + +import com.iemr.inventory.utils.mapper.OutputMapper; + +/** + * Shared harness that exercises the read/write accessors, the constructors and the + * {@code Object} overrides of the plain data carriers in this service. + * + *

These classes are pure state: hand-written getters and setters over JPA/Gson annotated + * fields. Asserting each pair by hand would be thousands of near-identical lines, so the + * harness reflects over every property instead and checks the same contract for all of them: + * what a setter stores is what the matching getter hands back.

+ */ +public final class BeanAccessorHarness { + + static { + // M_Uom-style toString() implementations delegate to OutputMapper.gson(), which only + // initialises its shared GsonBuilder from the constructor. Build one up front so the + // toString() checks below exercise real serialisation rather than tripping over a null builder. + new OutputMapper(); + } + + private static final Map NESTED_SAMPLES = new java.util.concurrent.ConcurrentHashMap<>(); + + private BeanAccessorHarness() { + } + + /** Discovers every concrete class under the given package, sorted for a stable test order. */ + public static List> classesIn(String basePackage) { + ClassPathScanningCandidateComponentProvider scanner = + new ClassPathScanningCandidateComponentProvider(false); + scanner.addIncludeFilter(new RegexPatternTypeFilter(java.util.regex.Pattern.compile(".*"))); + + List> classes = new ArrayList<>(); + for (BeanDefinition definition : scanner.findCandidateComponents(basePackage)) { + try { + Class type = Class.forName(definition.getBeanClassName()); + if (type.isInterface() || type.isEnum() || type.isAnonymousClass() + || Modifier.isAbstract(type.getModifiers()) + || type.getName().endsWith("Test") || type.getName().endsWith("Harness")) { + continue; + } + classes.add(type); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("scanned class could not be loaded", e); + } + } + classes.sort(Comparator.comparing(Class::getName)); + return classes; + } + + /** + * Instantiates the class, round-trips every getter/setter pair, and calls the + * {@code Object} overrides. Fails if a setter and its getter disagree. + */ + public static void verify(Class type) { + Object instance = instantiate(type); + assertNotNull(instance, () -> "could not instantiate " + type.getName()); + + Map getters = new HashMap<>(); + for (Method method : type.getMethods()) { + if (method.getDeclaringClass() == Object.class || method.getParameterCount() != 0 + || method.getReturnType() == void.class || Modifier.isStatic(method.getModifiers())) { + continue; + } + if (method.getName().startsWith("get")) { + getters.put(method.getName().substring(3), method); + } else if (method.getName().startsWith("is") && isBoolean(method.getReturnType())) { + getters.put(method.getName().substring(2), method); + } + } + + for (Method setter : type.getMethods()) { + if (!setter.getName().startsWith("set") || setter.getParameterCount() != 1 + || Modifier.isStatic(setter.getModifiers()) || setter.getDeclaringClass() == Object.class) { + continue; + } + + Class propertyType = setter.getParameterTypes()[0]; + Object value = sampleFor(propertyType, setter.getGenericParameterTypes()[0]); + + try { + setter.invoke(instance, value); + } catch (Exception e) { + throw new AssertionError("setter " + type.getSimpleName() + "." + setter.getName() + " threw", e); + } + + Method getter = getters.get(setter.getName().substring(3)); + if (getter == null || !isCompatible(getter.getReturnType(), propertyType)) { + continue; + } + + Object read; + try { + read = getter.invoke(instance); + } catch (Exception e) { + throw new AssertionError("getter " + type.getSimpleName() + "." + getter.getName() + " threw", e); + } + assertEquals(value, read, + () -> type.getSimpleName() + "." + getter.getName() + " must return what the setter stored"); + } + + // Read every remaining getter once, so derived and read-only properties are exercised too. + for (Method getter : getters.values()) { + assertDoesNotThrow(() -> getter.invoke(instance), + () -> type.getSimpleName() + "." + getter.getName() + " must not throw"); + } + + assertDoesNotThrow(instance::hashCode, () -> type.getSimpleName() + ".hashCode() must not throw"); + assertDoesNotThrow(() -> instance.equals(instance), () -> type.getSimpleName() + ".equals() must not throw"); + assertDoesNotThrow(() -> instance.equals(null), () -> type.getSimpleName() + ".equals(null) must not throw"); + assertDoesNotThrow(() -> instance.equals("a string"), + () -> type.getSimpleName() + ".equals() must reject a foreign type without throwing"); + assertDoesNotThrow(instance::toString, () -> type.getSimpleName() + ".toString() must not throw"); + } + + /** + * Exercises a value-semantics {@code equals}/{@code hashCode} pair: two identically populated + * instances must match, and changing any single property must break the match. Classes that do + * not override {@code equals} keep identity semantics and are skipped. + */ + public static void verifyValueSemantics(Class type) { + if (!overridesEquals(type)) { + return; + } + + Object left = instantiate(type); + Object right = instantiate(type); + if (left == null || right == null) { + return; + } + + List setters = writableProperties(type); + populate(left, setters, 0); + populate(right, setters, 0); + + assertEquals(left, right, () -> type.getSimpleName() + ": identically populated instances must be equal"); + assertEquals(left.hashCode(), right.hashCode(), + () -> type.getSimpleName() + ": equal instances must share a hash code"); + assertNotEquals(left, new Object(), () -> type.getSimpleName() + " must not equal a foreign type"); + assertNotEquals(left, null, () -> type.getSimpleName() + " must not equal null"); + + for (Method setter : setters) { + Class propertyType = setter.getParameterTypes()[0]; + Object original = sampleFor(propertyType, setter.getGenericParameterTypes()[0], 0); + Object different = sampleFor(propertyType, setter.getGenericParameterTypes()[0], 1); + if (different == null || different.equals(original)) { + continue; + } + + invoke(setter, right, different); + assertNotEquals(left, right, + () -> type.getSimpleName() + ": instances differing on " + setter.getName() + " must not be equal"); + invoke(setter, right, original); + } + + assertEquals(left, right, () -> type.getSimpleName() + ": restoring every property must restore equality"); + + verifyUnsetValueSemantics(type, setters); + } + + /** + * The mirror of the populated comparison: two untouched instances must be equal, and setting a + * single property on one of them must break that. This walks the "one side still holds the + * default" half of every field comparison, which the populated pass never reaches. + */ + private static void verifyUnsetValueSemantics(Class type, List setters) { + Object left = instantiate(type); + Object right = instantiate(type); + if (left == null || right == null) { + return; + } + + // A few carriers hold a helper object (an OutputMapper, say) built fresh in the field + // initialiser. Two of those are never equal, so give both sides the same shared instance + // before comparing; what is under test here is the declared properties, not the helpers. + for (Method setter : setters) { + Object leftDefault = readProperty(left, setter); + Object rightDefault = readProperty(right, setter); + if (leftDefault != null && !leftDefault.equals(rightDefault)) { + Object shared = sampleFor(setter.getParameterTypes()[0], setter.getGenericParameterTypes()[0], 0); + invoke(setter, left, shared); + invoke(setter, right, shared); + } + } + + assertEquals(left, right, () -> type.getSimpleName() + ": two untouched instances must be equal"); + assertEquals(left.hashCode(), right.hashCode(), + () -> type.getSimpleName() + ": two untouched instances must share a hash code"); + + for (Method setter : setters) { + Object value = sampleFor(setter.getParameterTypes()[0], setter.getGenericParameterTypes()[0], 0); + Object unset = readProperty(left, setter); + if (value == null || value.equals(unset)) { + continue; + } + + invoke(setter, right, value); + assertNotEquals(left, right, () -> type.getSimpleName() + ": setting only " + setter.getName() + + " on one instance must break equality"); + assertNotEquals(right, left, () -> type.getSimpleName() + ": setting only " + setter.getName() + + " on one instance must break equality in both directions"); + invoke(setter, right, unset); + } + + assertEquals(left, right, () -> type.getSimpleName() + ": clearing every property must restore equality"); + } + + /** Reads back the property a setter writes, or null when there is no matching readable getter. */ + private static Object readProperty(Object instance, Method setter) { + String property = setter.getName().substring(3); + for (String prefix : new String[] { "get", "is" }) { + try { + Method getter = instance.getClass().getMethod(prefix + property); + if (getter.getParameterCount() == 0 + && isCompatible(getter.getReturnType(), setter.getParameterTypes()[0])) { + return getter.invoke(instance); + } + } catch (NoSuchMethodException e) { + // try the other prefix + } catch (Exception e) { + return null; + } + } + return null; + } + + private static boolean overridesEquals(Class type) { + try { + return type.getMethod("equals", Object.class).getDeclaringClass() != Object.class; + } catch (NoSuchMethodException e) { + return false; + } + } + + private static List writableProperties(Class type) { + List setters = new ArrayList<>(); + for (Method method : type.getMethods()) { + if (method.getName().startsWith("set") && method.getParameterCount() == 1 + && !Modifier.isStatic(method.getModifiers()) && method.getDeclaringClass() != Object.class) { + setters.add(method); + } + } + setters.sort(Comparator.comparing(Method::getName)); + return setters; + } + + private static void populate(Object instance, List setters, int variant) { + for (Method setter : setters) { + invoke(setter, instance, + sampleFor(setter.getParameterTypes()[0], setter.getGenericParameterTypes()[0], variant)); + } + } + + private static void invoke(Method setter, Object target, Object value) { + try { + setter.invoke(target, value); + } catch (Exception e) { + throw new AssertionError("setter " + setter.getName() + " threw", e); + } + } + + /** Invokes every public constructor with sample arguments, to cover the all-args variants. */ + public static void verifyConstructors(Class type) { + for (Constructor constructor : type.getConstructors()) { + Object[] args = Arrays.stream(constructor.getParameters()) + .map(BeanAccessorHarness::sampleForParameter) + .toArray(); + assertDoesNotThrow(() -> constructor.newInstance(args), + () -> type.getSimpleName() + " constructor with " + args.length + " args must not throw"); + } + } + + private static Object sampleForParameter(Parameter parameter) { + return sampleFor(parameter.getType(), parameter.getParameterizedType()); + } + + private static Object instantiate(Class type) { + Constructor[] constructors = Arrays.stream(type.getConstructors()) + .sorted(Comparator.comparingInt(Constructor::getParameterCount)) + .toArray(Constructor[]::new); + + for (Constructor constructor : constructors) { + try { + Object[] args = Arrays.stream(constructor.getParameters()) + .map(BeanAccessorHarness::sampleForParameter) + .toArray(); + return constructor.newInstance(args); + } catch (Exception ignored) { + // try the next constructor + } + } + return null; + } + + private static boolean isBoolean(Class type) { + return type == boolean.class || type == Boolean.class; + } + + private static boolean isCompatible(Class getterType, Class setterType) { + if (getterType == setterType) { + return true; + } + return box(getterType) == box(setterType); + } + + private static Class box(Class type) { + if (type == int.class) return Integer.class; + if (type == long.class) return Long.class; + if (type == double.class) return Double.class; + if (type == float.class) return Float.class; + if (type == short.class) return Short.class; + if (type == byte.class) return Byte.class; + if (type == char.class) return Character.class; + if (type == boolean.class) return Boolean.class; + return type; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static Object sampleFor(Class type, Type genericType) { + return sampleFor(type, genericType, 0); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static Object sampleFor(Class type, Type genericType, int variant) { + if (type == String.class) return variant == 0 ? "sample" : "other-sample"; + if (type == int.class || type == Integer.class) return variant == 0 ? 7 : 13; + if (type == long.class || type == Long.class) return variant == 0 ? 7L : 13L; + if (type == double.class || type == Double.class) return variant == 0 ? 7.5d : 13.5d; + if (type == float.class || type == Float.class) return variant == 0 ? 7.5f : 13.5f; + if (type == short.class || type == Short.class) return (short) (variant == 0 ? 7 : 13); + if (type == byte.class || type == Byte.class) return (byte) (variant == 0 ? 7 : 13); + if (type == char.class || type == Character.class) return variant == 0 ? 'N' : 'Y'; + if (type == boolean.class || type == Boolean.class) return variant == 0 ? Boolean.TRUE : Boolean.FALSE; + if (type == BigDecimal.class) return BigDecimal.valueOf(variant == 0 ? 7.5d : 13.5d); + if (type == BigInteger.class) return BigInteger.valueOf(variant == 0 ? 7L : 13L); + if (type == java.sql.Date.class) return java.sql.Date.valueOf(variant == 0 ? "2025-01-31" : "2024-02-29"); + if (type == java.sql.Time.class) return java.sql.Time.valueOf(variant == 0 ? "10:15:30" : "22:45:00"); + if (type == Timestamp.class) { + return Timestamp.valueOf(variant == 0 ? "2025-01-31 10:15:30" : "2024-02-29 22:45:00"); + } + if (type == java.util.Date.class) return new java.util.Date(variant == 0 ? 1738300000000L : 1709200000000L); + if (type == java.time.LocalDate.class) { + return variant == 0 ? java.time.LocalDate.of(2025, 1, 31) : java.time.LocalDate.of(2024, 2, 29); + } + if (type == java.time.LocalDateTime.class) { + return variant == 0 ? java.time.LocalDateTime.of(2025, 1, 31, 10, 15) + : java.time.LocalDateTime.of(2024, 2, 29, 22, 45); + } + if (type == Object.class) return variant == 0 ? "sample" : "other-sample"; + if (type.isEnum()) { + Object[] constants = type.getEnumConstants(); + return constants.length > 0 ? constants[0] : null; + } + if (type.isArray()) { + Object array = Array.newInstance(type.getComponentType(), 1); + Array.set(array, 0, sampleFor(type.getComponentType(), type.getComponentType(), variant)); + return array; + } + if (List.class.isAssignableFrom(type) || Iterable.class == type || Collection(type)) { + List list = new ArrayList(); + Object element = elementSample(genericType, variant); + if (element != null) { + list.add(element); + } + return list; + } + if (Set.class.isAssignableFrom(type)) { + Set set = new LinkedHashSet(); + Object element = elementSample(genericType, variant); + if (element != null) { + set.add(element); + } + return set; + } + if (Map.class.isAssignableFrom(type)) { + return new HashMap<>(); + } + // Nested carriers are shared per (type, variant): many of them keep identity equality, so two + // separately built instances would never compare equal and would break the value-semantics check. + return NESTED_SAMPLES.computeIfAbsent(type.getName() + "#" + variant, key -> instantiate(type)); + } + + private static boolean Collection(Class type) { + return java.util.Collection.class == type; + } + + private static Object elementSample(Type genericType, int variant) { + if (genericType instanceof ParameterizedType parameterized) { + Type[] arguments = parameterized.getActualTypeArguments(); + if (arguments.length == 1 && arguments[0] instanceof Class elementType) { + return sampleFor(elementType, elementType, variant); + } + } + return null; + } + + /** Formats a class list for a JUnit {@code @MethodSource}. */ + public static List> concat(List>... groups) { + return Arrays.stream(groups).flatMap(List::stream) + .collect(Collectors.toCollection(() -> new ArrayList<>(new HashSet<>()))); + } +} diff --git a/src/test/java/com/iemr/inventory/support/DataModelAccessorTest.java b/src/test/java/com/iemr/inventory/support/DataModelAccessorTest.java new file mode 100644 index 00000000..0c9f1eaa --- /dev/null +++ b/src/test/java/com/iemr/inventory/support/DataModelAccessorTest.java @@ -0,0 +1,80 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.support; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + + +/** + * Covers the plain data carriers of the service - the JPA entities, the transfer objects and + * the report models - by round-tripping every accessor pair on every one of them. + */ +@DisplayName("Data model accessor Test Suite") +class DataModelAccessorTest { + + private static final String[] MODEL_PACKAGES = { + "com.iemr.inventory.data", + "com.iemr.inventory.model", + "com.iemr.inventory.to" + }; + + static Stream> modelClasses() { + return Stream.of(MODEL_PACKAGES) + .map(BeanAccessorHarness::classesIn) + .flatMap(List::stream); + } + + @Test + @DisplayName("the scan should actually find the model classes it is meant to cover") + void modelScan_shouldFindClasses() { + assertFalse(modelClasses().toList().isEmpty(), "no model classes were discovered on the test classpath"); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("modelClasses") + @DisplayName("every model should return from its getters exactly what its setters stored") + void model_shouldRoundTripAccessors(Class type) { + BeanAccessorHarness.verify(type); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("modelClasses") + @DisplayName("every model that defines value equality should honour it property by property") + void model_shouldHonourValueSemantics(Class type) { + BeanAccessorHarness.verifyValueSemantics(type); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("modelClasses") + @DisplayName("every model constructor should build an instance without throwing") + void model_shouldConstructWithoutThrowing(Class type) { + BeanAccessorHarness.verifyConstructors(type); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/CommonMainTest.java b/src/test/java/com/iemr/inventory/utils/CommonMainTest.java new file mode 100644 index 00000000..f187df34 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/CommonMainTest.java @@ -0,0 +1,60 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; + +@DisplayName("CommonMain Test Suite") +class CommonMainTest { + + private CommonMain commonMain; + + @BeforeEach + @DisplayName("Create the bean configuration before each test") + void setUp() { + commonMain = new CommonMain(); + } + + @Test + @DisplayName("configProperties should supply a fresh properties holder on each call") + void configProperties_shouldSupplyFreshHolder() { + assertNotNull(commonMain.configProperties()); + assertNotSame(commonMain.configProperties(), commonMain.configProperties()); + } + + @Test + @DisplayName("redisSession should supply a Spring Session Redis configuration") + void redisSession_shouldSupplySessionConfiguration() { + assertNotNull(commonMain.redisSession()); + } + + @Test + @DisplayName("redisStorage should supply a Redis store bean") + void redisStorage_shouldSupplyRedisStore() { + assertNotNull(commonMain.redisStorage()); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/CookieUtilTest.java b/src/test/java/com/iemr/inventory/utils/CookieUtilTest.java new file mode 100644 index 00000000..28a4055c --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/CookieUtilTest.java @@ -0,0 +1,115 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +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.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + + +@ExtendWith(MockitoExtension.class) +@DisplayName("CookieUtil Test Suite") +class CookieUtilTest { + + @Mock + HttpServletRequest request; + + @InjectMocks + CookieUtil cookieUtil; + + @Test + @DisplayName("Should return cookie value when cookie exists") + void getCookieValue_cookieExists() { + Cookie cookie = mock(Cookie.class); + doReturn("myCookieName").when(cookie).getName(); + doReturn("myCookieValue").when(cookie).getValue(); + doReturn(new Cookie[]{cookie}).when(request).getCookies(); + + Optional result = cookieUtil.getCookieValue(request, "myCookieName"); + + assertTrue(result.isPresent()); + assertEquals("myCookieValue", result.get()); + } + + @Test + @DisplayName("Should return empty Optional when cookie does not exist") + void getCookieValue_cookieDoesNotExist() { + doReturn(new Cookie[0]).when(request).getCookies(); + Optional result = cookieUtil.getCookieValue(request, "myCookieName"); + assertFalse(result.isPresent()); + } + + @Test + @DisplayName("Should return empty Optional when cookies array is null") + void getCookieValue_cookiesIsNull() { + doReturn(null).when(request).getCookies(); + Optional result = cookieUtil.getCookieValue(request, "myCookieName"); + assertFalse(result.isPresent()); + } + + + @Test + @DisplayName("Should return JWT token when JWT cookie exists") + void getJwtTokenFromCookie_jwtCookieExists() { + Cookie jwtCookie = mock(Cookie.class); + doReturn("Jwttoken").when(jwtCookie).getName(); + doReturn("myJwtToken").when(jwtCookie).getValue(); + doReturn(new Cookie[]{jwtCookie}).when(request).getCookies(); + + String jwtToken = CookieUtil.getJwtTokenFromCookie(request); + + assertEquals("myJwtToken", jwtToken); + } + + @Test + @DisplayName("Should return null when JWT cookie does not exist") + void getJwtTokenFromCookie_jwtCookieDoesNotExist() { + Cookie otherCookie = mock(Cookie.class); + doReturn("otherCookie").when(otherCookie).getName(); + // doReturn("otherValue").when(otherCookie).getValue(); + doReturn(new Cookie[]{otherCookie}).when(request).getCookies(); + + String jwtToken = CookieUtil.getJwtTokenFromCookie(request); + + assertNull(jwtToken); + } + + @Test + @DisplayName("Should return null when cookies array is null for JWT token lookup") + void getJwtTokenFromCookie_cookiesIsNull() { + doReturn(null).when(request).getCookies(); + String jwtToken = CookieUtil.getJwtTokenFromCookie(request); + assertNull(jwtToken); + } +} \ No newline at end of file diff --git a/src/test/java/com/iemr/inventory/utils/FilterConfigTest.java b/src/test/java/com/iemr/inventory/utils/FilterConfigTest.java new file mode 100644 index 00000000..5f97ba43 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/FilterConfigTest.java @@ -0,0 +1,84 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +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 org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.core.Ordered; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@ExtendWith(MockitoExtension.class) +@DisplayName("FilterConfig Test Suite") +class FilterConfigTest { + + private static final String ALLOWED_ORIGINS = "https://amrit.example.org,http://localhost:*"; + + @Mock + private JwtAuthenticationUtil jwtAuthenticationUtil; + + private FilterConfig filterConfig; + + @BeforeEach + @DisplayName("Configure the allow-list before each test") + void setUp() { + filterConfig = new FilterConfig(); + ReflectionTestUtils.setField(filterConfig, "allowedOrigins", ALLOWED_ORIGINS); + } + + @Test + @DisplayName("jwtUserIdValidationFilter should register the JWT filter across every url pattern") + void jwtUserIdValidationFilter_shouldRegisterFilterForEveryUrlPattern() { + FilterRegistrationBean registration = + filterConfig.jwtUserIdValidationFilter(jwtAuthenticationUtil); + + assertNotNull(registration.getFilter()); + assertEquals(java.util.Set.of("/*"), registration.getUrlPatterns()); + } + + @Test + @DisplayName("jwtUserIdValidationFilter should run at the highest precedence so auth happens first") + void jwtUserIdValidationFilter_shouldRunAtHighestPrecedence() { + FilterRegistrationBean registration = + filterConfig.jwtUserIdValidationFilter(jwtAuthenticationUtil); + + assertEquals(Ordered.HIGHEST_PRECEDENCE, registration.getOrder()); + } + + @Test + @DisplayName("jwtUserIdValidationFilter should hand the filter the configured origins and auth util") + void jwtUserIdValidationFilter_shouldPassOriginsAndAuthUtilToFilter() { + JwtUserIdValidationFilter filter = + filterConfig.jwtUserIdValidationFilter(jwtAuthenticationUtil).getFilter(); + + assertEquals(ALLOWED_ORIGINS, ReflectionTestUtils.getField(filter, "allowedOrigins")); + assertSame(jwtAuthenticationUtil, ReflectionTestUtils.getField(filter, "jwtAuthenticationUtil")); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/IEMRApplBeansTest.java b/src/test/java/com/iemr/inventory/utils/IEMRApplBeansTest.java new file mode 100644 index 00000000..f595f8db --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/IEMRApplBeansTest.java @@ -0,0 +1,72 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import com.iemr.inventory.utils.gateway.email.EmailService; +import com.iemr.inventory.utils.gateway.email.GenericEmailServiceImpl; + +@DisplayName("IEMRApplBeans Test Suite") +class IEMRApplBeansTest { + + private IEMRApplBeans beans; + + @BeforeEach + @DisplayName("Create the bean configuration before each test") + void setUp() { + beans = new IEMRApplBeans(); + } + + @Test + @DisplayName("getEmailService should supply the generic email implementation") + void getEmailService_shouldSupplyGenericImplementation() { + EmailService emailService = beans.getEmailService(); + + assertNotNull(emailService); + assertInstanceOf(GenericEmailServiceImpl.class, emailService); + } + + @Test + @DisplayName("getJavaMailSender should supply a JavaMailSender implementation") + void getJavaMailSender_shouldSupplyMailSenderImplementation() { + JavaMailSender mailSender = beans.getJavaMailSender(); + + assertNotNull(mailSender); + assertInstanceOf(JavaMailSenderImpl.class, mailSender); + } + + @Test + @DisplayName("configProperties should supply a fresh properties holder on each call") + void configProperties_shouldSupplyFreshHolder() { + assertNotNull(beans.configProperties()); + assertNotSame(beans.configProperties(), beans.configProperties()); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/JwtAuthenticationUtilTest.java b/src/test/java/com/iemr/inventory/utils/JwtAuthenticationUtilTest.java new file mode 100644 index 00000000..1f47949c --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/JwtAuthenticationUtilTest.java @@ -0,0 +1,214 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +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.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +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 org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.inventory.data.user.M_User; +import com.iemr.inventory.repo.users.UserLoginRepo; +import com.iemr.inventory.utils.exception.IEMRException; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("JwtAuthenticationUtil Test Suite") +class JwtAuthenticationUtilTest { + + @Mock + private CookieUtil cookieUtil; + @Mock + private JwtUtil jwtUtil; + @Mock + private UserLoginRepo userLoginRepo; + @Mock + private RedisTemplate redisTemplate; + @Mock + private ValueOperations valueOperations; + @Mock + private HttpServletRequest request; + @Mock + private Claims claims; + + private JwtAuthenticationUtil jwtAuthenticationUtil; + + @BeforeEach + @DisplayName("Wire the util with mocked collaborators") + void setUp() { + jwtAuthenticationUtil = new JwtAuthenticationUtil(cookieUtil, jwtUtil); + ReflectionTestUtils.setField(jwtAuthenticationUtil, "userLoginRepo", userLoginRepo); + ReflectionTestUtils.setField(jwtAuthenticationUtil, "redisTemplate", redisTemplate); + } + + @Test + @DisplayName("validateJwtToken should return 401 when the Jwttoken cookie is absent") + void validateJwtToken_shouldReturnUnauthorizedWhenCookieMissing() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.empty()); + + ResponseEntity result = jwtAuthenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.UNAUTHORIZED, result.getStatusCode()); + assertEquals("Error 401: Unauthorized - JWT Token is not set!", result.getBody()); + verify(jwtUtil, never()).validateToken(anyString()); + } + + @Test + @DisplayName("validateJwtToken should return 401 when the token fails validation") + void validateJwtToken_shouldReturnUnauthorizedWhenTokenInvalid() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.of("bad-token")); + when(jwtUtil.validateToken("bad-token")).thenReturn(null); + + ResponseEntity result = jwtAuthenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.UNAUTHORIZED, result.getStatusCode()); + assertEquals("Error 401: Unauthorized - Invalid JWT Token!", result.getBody()); + } + + @Test + @DisplayName("validateJwtToken should return 401 when the subject claim is null") + void validateJwtToken_shouldReturnUnauthorizedWhenSubjectNull() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.of("token")); + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(claims.getSubject()).thenReturn(null); + + ResponseEntity result = jwtAuthenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.UNAUTHORIZED, result.getStatusCode()); + assertEquals("Error 401: Unauthorized - Username is missing!", result.getBody()); + } + + @Test + @DisplayName("validateJwtToken should return 401 when the subject claim is blank") + void validateJwtToken_shouldReturnUnauthorizedWhenSubjectEmpty() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.of("token")); + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(claims.getSubject()).thenReturn(""); + + ResponseEntity result = jwtAuthenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.UNAUTHORIZED, result.getStatusCode()); + } + + @Test + @DisplayName("validateJwtToken should return 200 with the username for a valid token") + void validateJwtToken_shouldReturnUsernameWhenValid() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.of("token")); + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(claims.getSubject()).thenReturn("john.doe"); + + ResponseEntity result = jwtAuthenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("john.doe", result.getBody()); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should pass when the user is already cached in Redis") + void validateUserIdAndJwtToken_shouldReturnTrueWhenUserCached() throws Exception { + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(claims.get("userId", String.class)).thenReturn("11"); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get("user_11")).thenReturn(new M_User()); + + assertTrue(jwtAuthenticationUtil.validateUserIdAndJwtToken("token")); + verify(userLoginRepo, never()).getUserByUserID(anyLong()); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should fall back to the DB and cache the user when Redis misses") + void validateUserIdAndJwtToken_shouldFetchFromDbAndCache() throws Exception { + M_User dbUser = new M_User(); + dbUser.setUserID(11); + dbUser.setUserName("john.doe"); + + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(claims.get("userId", String.class)).thenReturn("11"); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get("user_11")).thenReturn(null); + when(userLoginRepo.getUserByUserID(11L)).thenReturn(dbUser); + + assertTrue(jwtAuthenticationUtil.validateUserIdAndJwtToken("token")); + verify(valueOperations).set(eq("user_11"), any(M_User.class), eq(30L), eq(TimeUnit.MINUTES)); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should throw IEMRException when the token is invalid") + void validateUserIdAndJwtToken_shouldThrowWhenTokenInvalid() { + when(jwtUtil.validateToken("token")).thenReturn(null); + + IEMRException ex = assertThrows(IEMRException.class, + () -> jwtAuthenticationUtil.validateUserIdAndJwtToken("token")); + assertTrue(ex.getMessage().contains("Invalid JWT token.")); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should throw IEMRException when neither Redis nor the DB knows the user") + void validateUserIdAndJwtToken_shouldThrowWhenUserUnknown() { + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(claims.get("userId", String.class)).thenReturn("99"); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get("user_99")).thenReturn(null); + when(userLoginRepo.getUserByUserID(99L)).thenReturn(null); + + IEMRException ex = assertThrows(IEMRException.class, + () -> jwtAuthenticationUtil.validateUserIdAndJwtToken("token")); + assertTrue(ex.getMessage().contains("Invalid User ID.")); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should wrap a non-numeric userId claim in an IEMRException") + void validateUserIdAndJwtToken_shouldThrowWhenUserIdNotNumeric() { + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(claims.get("userId", String.class)).thenReturn("not-a-number"); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get("user_not-a-number")).thenReturn(null); + + assertThrows(IEMRException.class, () -> jwtAuthenticationUtil.validateUserIdAndJwtToken("token")); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/JwtUserIdValidationFilterTest.java b/src/test/java/com/iemr/inventory/utils/JwtUserIdValidationFilterTest.java new file mode 100644 index 00000000..2fc0f873 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/JwtUserIdValidationFilterTest.java @@ -0,0 +1,447 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import com.iemr.inventory.utils.http.AuthorizationHeaderRequestWrapper; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +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.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("JwtUserIdValidationFilter Test Suite") +class JwtUserIdValidationFilterTest { + + private static final String ALLOWED_ORIGINS = "https://amrit.example.org,http://localhost:*"; + private static final String ALLOWED_ORIGIN = "https://amrit.example.org"; + private static final String DISALLOWED_ORIGIN = "https://evil.example.com"; + + @Mock + private JwtAuthenticationUtil jwtAuthenticationUtil; + + @Mock + private FilterChain filterChain; + + private JwtUserIdValidationFilter filter; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @BeforeEach + @DisplayName("Set up the filter with a configured allow-list before each test") + void setUp() { + filter = new JwtUserIdValidationFilter(jwtAuthenticationUtil, ALLOWED_ORIGINS); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + } + + @Nested + @DisplayName("Origin validation and CORS") + class OriginValidationTests { + + @Test + @DisplayName("doFilter should reject an OPTIONS request that carries no Origin header") + void doFilter_shouldRejectOptionsWithoutOrigin() throws Exception { + request.setMethod("OPTIONS"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + assertEquals("OPTIONS request requires Origin header", response.getErrorMessage()); + verify(filterChain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } + + @Test + @DisplayName("doFilter should reject an OPTIONS request from an origin outside the allow-list") + void doFilter_shouldRejectOptionsFromDisallowedOrigin() throws Exception { + request.setMethod("OPTIONS"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("Origin", DISALLOWED_ORIGIN); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + assertEquals("Origin not allowed", response.getErrorMessage()); + verify(filterChain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } + + @Test + @DisplayName("doFilter should answer an allowed OPTIONS preflight with 200 and the CORS headers") + void doFilter_shouldAnswerAllowedPreflightWithCorsHeaders() throws Exception { + request.setMethod("OPTIONS"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("Origin", ALLOWED_ORIGIN); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertEquals(ALLOWED_ORIGIN, response.getHeader("Access-Control-Allow-Origin")); + assertEquals("GET, POST, PUT, PATCH, DELETE, OPTIONS", + response.getHeader("Access-Control-Allow-Methods")); + assertEquals("true", response.getHeader("Access-Control-Allow-Credentials")); + assertEquals("3600", response.getHeader("Access-Control-Max-Age")); + assertNotNull(response.getHeader("Access-Control-Allow-Headers")); + verify(filterChain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } + + @Test + @DisplayName("doFilter should match a wildcard localhost origin pattern") + void doFilter_shouldMatchWildcardLocalhostOrigin() throws Exception { + request.setMethod("OPTIONS"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("Origin", "http://localhost:4200"); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertEquals("http://localhost:4200", response.getHeader("Access-Control-Allow-Origin")); + } + + @Test + @DisplayName("doFilter should reject a non-OPTIONS request from an origin outside the allow-list") + void doFilter_shouldRejectNonOptionsFromDisallowedOrigin() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("Origin", DISALLOWED_ORIGIN); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + assertEquals("Origin not allowed", response.getErrorMessage()); + verify(filterChain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } + + @Test + @DisplayName("doFilter should treat every origin as disallowed when no allow-list is configured") + void doFilter_shouldRejectAllOriginsWhenAllowListIsBlank() throws Exception { + JwtUserIdValidationFilter unconfiguredFilter = + new JwtUserIdValidationFilter(jwtAuthenticationUtil, " "); + request.setMethod("OPTIONS"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("Origin", ALLOWED_ORIGIN); + + unconfiguredFilter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + @Test + @DisplayName("doFilter should not add CORS headers when the request carries no Origin header") + void doFilter_shouldNotAddCorsHeadersWithoutOrigin() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/health"); + + filter.doFilter(request, response, filterChain); + + assertNull(response.getHeader("Access-Control-Allow-Origin")); + verify(filterChain).doFilter(request, response); + } + } + + @Nested + @DisplayName("Public endpoints that bypass token validation") + class PublicEndpointTests { + + @Test + @DisplayName("doFilter should pass /health straight through without validating a token") + void doFilter_shouldSkipValidationForHealth() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/health"); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verify(jwtAuthenticationUtil, never()).validateUserIdAndJwtToken(anyString()); + } + + @Test + @DisplayName("doFilter should pass /version straight through without validating a token") + void doFilter_shouldSkipValidationForVersion() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/version"); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verify(jwtAuthenticationUtil, never()).validateUserIdAndJwtToken(anyString()); + } + + @Test + @DisplayName("doFilter should pass the login endpoint straight through without validating a token") + void doFilter_shouldSkipValidationForUserAuthenticate() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/user/userAuthenticate"); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verify(jwtAuthenticationUtil, never()).validateUserIdAndJwtToken(anyString()); + } + + @Test + @DisplayName("doFilter should pass any /public path straight through without validating a token") + void doFilter_shouldSkipValidationForPublicPaths() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/public/anything"); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verify(jwtAuthenticationUtil, never()).validateUserIdAndJwtToken(anyString()); + } + + @Test + @DisplayName("doFilter should pass the concurrent-session logout endpoint straight through") + void doFilter_shouldSkipValidationForConcurrentSessionLogout() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/user/logOutUserFromConcurrentSession"); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verify(jwtAuthenticationUtil, never()).validateUserIdAndJwtToken(anyString()); + } + } + + @Nested + @DisplayName("JWT token validation") + class TokenValidationTests { + + @Test + @DisplayName("doFilter should continue the chain when the cookie token is valid") + void doFilter_shouldContinueChainWhenCookieTokenIsValid() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.setCookies(new Cookie("Jwttoken", "cookie-token")); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken("cookie-token")).thenReturn(true); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(AuthorizationHeaderRequestWrapper.class), any(ServletResponse.class)); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + } + + @Test + @DisplayName("doFilter should reject with 401 when the cookie token is rejected") + void doFilter_shouldRejectWhenCookieTokenIsInvalid() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.setCookies(new Cookie("Jwttoken", "cookie-token")); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken("cookie-token")).thenReturn(false); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + assertEquals("Unauthorized: Invalid or missing token", response.getErrorMessage()); + verify(filterChain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } + + @Test + @DisplayName("doFilter should continue the chain when the header token is valid") + void doFilter_shouldContinueChainWhenHeaderTokenIsValid() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("JwtToken", "header-token"); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken("header-token")).thenReturn(true); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(AuthorizationHeaderRequestWrapper.class), any(ServletResponse.class)); + } + + @Test + @DisplayName("doFilter should reject with 401 when the header token is rejected") + void doFilter_shouldRejectWhenHeaderTokenIsInvalid() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("JwtToken", "header-token"); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken("header-token")).thenReturn(false); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + verify(filterChain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } + + @Test + @DisplayName("doFilter should reject with 401 when no token is present at all") + void doFilter_shouldRejectWhenNoTokenPresent() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + assertEquals("Unauthorized: Invalid or missing token", response.getErrorMessage()); + } + + @Test + @DisplayName("doFilter should surface a 401 carrying the message when validation throws") + void doFilter_shouldRejectWithErrorMessageWhenValidationThrows() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("JwtToken", "header-token"); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken("header-token")) + .thenThrow(new IllegalStateException("token expired")); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + assertTrue(response.getErrorMessage().contains("token expired"), + "error message should carry the underlying cause"); + } + } + + @Nested + @DisplayName("Mobile client handling") + class MobileClientTests { + + @Test + @DisplayName("doFilter should let an okhttp client through on its Authorization header alone") + void doFilter_shouldAllowOkHttpClientWithAuthorizationHeader() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("User-Agent", "okhttp/4.9.0"); + request.addHeader("Authorization", "some-session-key"); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verify(jwtAuthenticationUtil, never()).validateUserIdAndJwtToken(anyString()); + } + + @Test + @DisplayName("doFilter should reject a plain java/ client, which is not recognised as a mobile client") + void doFilter_shouldRejectJavaClientWithAuthorizationHeader() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("User-Agent", "Java/17.0.2"); + request.addHeader("Authorization", "some-session-key"); + + filter.doFilter(request, response, filterChain); + + verify(filterChain, never()).doFilter(request, response); + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + } + + @Test + @DisplayName("doFilter should clear the User-Agent context once the mobile request completes") + void doFilter_shouldClearUserAgentContextAfterMobileRequest() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("User-Agent", "okhttp/4.9.0"); + request.addHeader("Authorization", "some-session-key"); + + filter.doFilter(request, response, filterChain); + + assertNull(UserAgentContext.getUserAgent(), + "the thread-local User-Agent must not leak past the request"); + } + + @Test + @DisplayName("doFilter should reject a browser client that has no token") + void doFilter_shouldRejectBrowserClientWithoutToken() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("User-Agent", "Mozilla/5.0"); + request.addHeader("Authorization", "some-session-key"); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + verify(filterChain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } + + @Test + @DisplayName("doFilter should reject a mobile client that sends no Authorization header") + void doFilter_shouldRejectMobileClientWithoutAuthorizationHeader() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/beneficiary/generateBeneficiaryIDs"); + request.addHeader("User-Agent", "okhttp/4.9.0"); + + filter.doFilter(request, response, filterChain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + } + } + + @Nested + @DisplayName("userId cookie hygiene") + class UserIdCookieTests { + + @Test + @DisplayName("doFilter should expire any userId cookie the client sends") + void doFilter_shouldExpireUserIdCookie() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/health"); + request.setCookies(new Cookie("userId", "1234")); + + filter.doFilter(request, response, filterChain); + + Cookie cleared = Arrays.stream(response.getCookies()) + .filter(cookie -> "userId".equals(cookie.getName())) + .findFirst() + .orElse(null); + assertNotNull(cleared, "a userId cookie should have been sent back to expire it"); + assertEquals(0, cleared.getMaxAge()); + assertEquals("/", cleared.getPath()); + assertTrue(cleared.isHttpOnly()); + assertTrue(cleared.getSecure()); + } + + @Test + @DisplayName("doFilter should leave unrelated cookies untouched") + void doFilter_shouldLeaveUnrelatedCookiesUntouched() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/health"); + request.setCookies(new Cookie("theme", "dark")); + + filter.doFilter(request, response, filterChain); + + assertEquals(0, response.getCookies().length); + } + } +} diff --git a/src/test/java/com/iemr/inventory/utils/JwtUtilTest.java b/src/test/java/com/iemr/inventory/utils/JwtUtilTest.java new file mode 100644 index 00000000..f562e347 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/JwtUtilTest.java @@ -0,0 +1,190 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +import java.util.Date; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("JwtUtil Test Suite") +class JwtUtilTest { + + private static final String SECRET = "amrit-bengen-test-secret-key-that-is-long-enough-for-hs256"; + private static final String OTHER_SECRET = "a-completely-different-secret-key-also-long-enough-for-hs256"; + + @Mock + private TokenDenylist tokenDenylist; + + private JwtUtil jwtUtil; + + @BeforeEach + @DisplayName("Wire the util with a test secret and a mocked denylist before each test") + void setUp() { + jwtUtil = new JwtUtil(); + ReflectionTestUtils.setField(jwtUtil, "SECRET_KEY", SECRET); + ReflectionTestUtils.setField(jwtUtil, "tokenDenylist", tokenDenylist); + } + + private String token(String secret, String subject, String jti, Date expiry) { + SecretKey key = Keys.hmacShaKeyFor(secret.getBytes()); + var builder = Jwts.builder().subject(subject).signWith(key); + if (jti != null) { + builder.id(jti); + } + if (expiry != null) { + builder.expiration(expiry); + } + return builder.compact(); + } + + private String validToken(String subject, String jti) { + return token(SECRET, subject, jti, new Date(System.currentTimeMillis() + 600_000)); + } + + @Nested + @DisplayName("validateToken") + class ValidateTokenTests { + + @Test + @DisplayName("validateToken should return the claims for a correctly signed token") + void validateToken_shouldReturnClaimsForValidToken() { + when(tokenDenylist.isTokenDenylisted("jti-1")).thenReturn(false); + + Claims claims = jwtUtil.validateToken(validToken("amrit-user", "jti-1")); + + assertNotNull(claims); + assertEquals("amrit-user", claims.getSubject()); + assertEquals("jti-1", claims.getId()); + } + + @Test + @DisplayName("validateToken should skip the denylist check for a token without a jti") + void validateToken_shouldSkipDenylistCheckWithoutJti() { + Claims claims = jwtUtil.validateToken(validToken("amrit-user", null)); + + assertNotNull(claims); + assertEquals("amrit-user", claims.getSubject()); + } + + @Test + @DisplayName("validateToken should reject a token whose jti has been denylisted") + void validateToken_shouldRejectDenylistedToken() { + when(tokenDenylist.isTokenDenylisted("jti-1")).thenReturn(true); + + assertNull(jwtUtil.validateToken(validToken("amrit-user", "jti-1"))); + } + + @Test + @DisplayName("validateToken should reject a token signed with a different secret") + void validateToken_shouldRejectTokenSignedWithDifferentSecret() { + String foreign = token(OTHER_SECRET, "amrit-user", "jti-1", + new Date(System.currentTimeMillis() + 600_000)); + + assertNull(jwtUtil.validateToken(foreign)); + } + + @Test + @DisplayName("validateToken should reject an expired token") + void validateToken_shouldRejectExpiredToken() { + String expired = token(SECRET, "amrit-user", "jti-1", + new Date(System.currentTimeMillis() - 60_000)); + + assertNull(jwtUtil.validateToken(expired)); + } + + @Test + @DisplayName("validateToken should reject a malformed token") + void validateToken_shouldRejectMalformedToken() { + assertNull(jwtUtil.validateToken("not-a-jwt")); + } + + @Test + @DisplayName("validateToken should reject a null token") + void validateToken_shouldRejectNullToken() { + assertNull(jwtUtil.validateToken(null)); + } + + @Test + @DisplayName("validateToken should reject every token when no secret is configured") + void validateToken_shouldRejectWhenSecretIsNotConfigured() { + String signed = validToken("amrit-user", null); + ReflectionTestUtils.setField(jwtUtil, "SECRET_KEY", null); + + assertNull(jwtUtil.validateToken(signed)); + } + } + + @Nested + @DisplayName("Claim extraction") + class ClaimExtractionTests { + + @Test + @DisplayName("extractUsername should return the token subject") + void extractUsername_shouldReturnSubject() { + assertEquals("amrit-user", jwtUtil.extractUsername(validToken("amrit-user", null))); + } + + @Test + @DisplayName("extractClaim should apply the supplied resolver to the claims") + void extractClaim_shouldApplySuppliedResolver() { + lenient().when(tokenDenylist.isTokenDenylisted("jti-9")).thenReturn(false); + + assertEquals("jti-9", jwtUtil.extractClaim(validToken("amrit-user", "jti-9"), Claims::getId)); + } + + @Test + @DisplayName("extractClaim should raise when the token cannot be parsed") + void extractClaim_shouldRaiseForMalformedToken() { + assertThrows(Exception.class, () -> jwtUtil.extractClaim("not-a-jwt", Claims::getSubject)); + } + + @Test + @DisplayName("extractUsername should raise when no secret is configured") + void extractUsername_shouldRaiseWhenSecretIsNotConfigured() { + String signed = validToken("amrit-user", null); + ReflectionTestUtils.setField(jwtUtil, "SECRET_KEY", ""); + + assertThrows(IllegalStateException.class, () -> jwtUtil.extractUsername(signed)); + } + } +} diff --git a/src/test/java/com/iemr/inventory/utils/RestTemplateUtilTest.java b/src/test/java/com/iemr/inventory/utils/RestTemplateUtilTest.java new file mode 100644 index 00000000..6c13aff5 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/RestTemplateUtilTest.java @@ -0,0 +1,158 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import jakarta.servlet.http.Cookie; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@DisplayName("RestTemplateUtil Test Suite") +class RestTemplateUtilTest { + + private static final String AUTHORIZATION = "session-key-123"; + private static final String BODY = "{\"benCount\":5}"; + private static final String JSON_UTF8 = "application/json;charset=utf-8"; + + private MockHttpServletRequest request; + + @BeforeEach + @DisplayName("Bind a fresh mock request to the request context before each test") + void setUp() { + request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + } + + @AfterEach + @DisplayName("Clear the request context and User-Agent thread local after each test") + void tearDown() { + RequestContextHolder.resetRequestAttributes(); + UserAgentContext.clear(); + } + + @Nested + @DisplayName("Outside a web request") + class NoRequestContextTests { + + @Test + @DisplayName("createRequestEntity should build a minimal entity when no request is bound") + void createRequestEntity_shouldBuildMinimalEntityWithoutRequestContext() { + RequestContextHolder.resetRequestAttributes(); + + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertSame(BODY, entity.getBody()); + assertEquals(JSON_UTF8, entity.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)); + assertEquals(AUTHORIZATION, entity.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + assertFalse(entity.getHeaders().containsKey("JwtToken")); + assertFalse(entity.getHeaders().containsKey(HttpHeaders.COOKIE)); + } + } + + @Nested + @DisplayName("Inside a web request") + class WithRequestContextTests { + + @Test + @DisplayName("createRequestEntity should carry the content type and authorization from the caller") + void createRequestEntity_shouldCarryContentTypeAndAuthorization() { + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertSame(BODY, entity.getBody()); + assertEquals(JSON_UTF8, entity.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)); + assertEquals(AUTHORIZATION, entity.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + @DisplayName("createRequestEntity should forward the inbound JwtToken header") + void createRequestEntity_shouldForwardInboundJwtTokenHeader() { + request.addHeader("JwtToken", "header-token"); + + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertEquals("header-token", entity.getHeaders().getFirst("JwtToken")); + } + + @Test + @DisplayName("createRequestEntity should replay the Jwttoken cookie as a Cookie header") + void createRequestEntity_shouldReplayJwtTokenCookie() { + request.setCookies(new Cookie("Jwttoken", "cookie-token")); + + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertEquals("Jwttoken=cookie-token", entity.getHeaders().getFirst(HttpHeaders.COOKIE)); + } + + @Test + @DisplayName("createRequestEntity should omit the Cookie header when no Jwttoken cookie is present") + void createRequestEntity_shouldOmitCookieHeaderWithoutJwtTokenCookie() { + request.setCookies(new Cookie("theme", "dark")); + + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertNull(entity.getHeaders().getFirst(HttpHeaders.COOKIE)); + } + + @Test + @DisplayName("createRequestEntity should propagate the mobile User-Agent when one is in scope") + void createRequestEntity_shouldPropagateMobileUserAgent() { + UserAgentContext.setUserAgent("okhttp/4.9.0"); + + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertEquals("okhttp/4.9.0", entity.getHeaders().getFirst(HttpHeaders.USER_AGENT)); + } + + @Test + @DisplayName("createRequestEntity should omit the User-Agent header when none is in scope") + void createRequestEntity_shouldOmitUserAgentWhenNoneInScope() { + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertNull(entity.getHeaders().getFirst(HttpHeaders.USER_AGENT)); + } + + @Test + @DisplayName("createRequestEntity should carry both the cookie and header tokens together") + void createRequestEntity_shouldCarryBothCookieAndHeaderTokens() { + request.addHeader("JwtToken", "header-token"); + request.setCookies(new Cookie("Jwttoken", "cookie-token")); + + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertEquals("header-token", entity.getHeaders().getFirst("JwtToken")); + assertEquals("Jwttoken=cookie-token", entity.getHeaders().getFirst(HttpHeaders.COOKIE)); + } + } +} diff --git a/src/test/java/com/iemr/inventory/utils/TokenDenylistTest.java b/src/test/java/com/iemr/inventory/utils/TokenDenylistTest.java new file mode 100644 index 00000000..50a1a239 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/TokenDenylistTest.java @@ -0,0 +1,185 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.assertFalse; +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.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("TokenDenylist Test Suite") +class TokenDenylistTest { + + private static final String JTI = "jti-1"; + private static final String KEY = "denied_jti-1"; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + private TokenDenylist tokenDenylist; + + @BeforeEach + @DisplayName("Wire the denylist with a mocked Redis template before each test") + void setUp() { + tokenDenylist = new TokenDenylist(); + ReflectionTestUtils.setField(tokenDenylist, "redisTemplate", redisTemplate); + } + + @Nested + @DisplayName("addTokenToDenylist") + class AddTokenTests { + + @Test + @DisplayName("addTokenToDenylist should store the prefixed key with the supplied expiry") + void addTokenToDenylist_shouldStorePrefixedKeyWithExpiry() { + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + + tokenDenylist.addTokenToDenylist(JTI, 60_000L); + + verify(valueOperations).set(KEY, " ", 60_000L, TimeUnit.MILLISECONDS); + } + + @Test + @DisplayName("addTokenToDenylist should ignore a null jti") + void addTokenToDenylist_shouldIgnoreNullJti() { + tokenDenylist.addTokenToDenylist(null, 60_000L); + + verifyNoInteractions(redisTemplate); + } + + @Test + @DisplayName("addTokenToDenylist should ignore a blank jti") + void addTokenToDenylist_shouldIgnoreBlankJti() { + tokenDenylist.addTokenToDenylist(" ", 60_000L); + + verifyNoInteractions(redisTemplate); + } + + @Test + @DisplayName("addTokenToDenylist should reject a null expiry") + void addTokenToDenylist_shouldRejectNullExpiry() { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> tokenDenylist.addTokenToDenylist(JTI, null)); + + assertTrue(thrown.getMessage().contains("Expiration time must be positive")); + verifyNoInteractions(redisTemplate); + } + + @Test + @DisplayName("addTokenToDenylist should reject a non-positive expiry") + void addTokenToDenylist_shouldRejectNonPositiveExpiry() { + assertThrows(IllegalArgumentException.class, () -> tokenDenylist.addTokenToDenylist(JTI, 0L)); + assertThrows(IllegalArgumentException.class, () -> tokenDenylist.addTokenToDenylist(JTI, -5L)); + verifyNoInteractions(redisTemplate); + } + + @Test + @DisplayName("addTokenToDenylist should surface a Redis failure as a runtime exception") + void addTokenToDenylist_shouldSurfaceRedisFailure() { + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + doThrow(new IllegalStateException("redis down")) + .when(valueOperations).set(anyString(), any(), anyLong(), any(TimeUnit.class)); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> tokenDenylist.addTokenToDenylist(JTI, 60_000L)); + + assertTrue(thrown.getMessage().contains("Failed to denylist token")); + } + } + + @Nested + @DisplayName("isTokenDenylisted") + class IsTokenDenylistedTests { + + @Test + @DisplayName("isTokenDenylisted should report true when the prefixed key exists") + void isTokenDenylisted_shouldReportTrueWhenKeyExists() { + when(redisTemplate.hasKey(KEY)).thenReturn(true); + + assertTrue(tokenDenylist.isTokenDenylisted(JTI)); + } + + @Test + @DisplayName("isTokenDenylisted should report false when the key does not exist") + void isTokenDenylisted_shouldReportFalseWhenKeyAbsent() { + when(redisTemplate.hasKey(KEY)).thenReturn(false); + + assertFalse(tokenDenylist.isTokenDenylisted(JTI)); + } + + @Test + @DisplayName("isTokenDenylisted should report false when Redis answers null") + void isTokenDenylisted_shouldReportFalseWhenRedisAnswersNull() { + when(redisTemplate.hasKey(KEY)).thenReturn(null); + + assertFalse(tokenDenylist.isTokenDenylisted(JTI)); + } + + @Test + @DisplayName("isTokenDenylisted should report false for a null jti without touching Redis") + void isTokenDenylisted_shouldReportFalseForNullJti() { + assertFalse(tokenDenylist.isTokenDenylisted(null)); + + verify(redisTemplate, never()).hasKey(anyString()); + } + + @Test + @DisplayName("isTokenDenylisted should report false for a blank jti without touching Redis") + void isTokenDenylisted_shouldReportFalseForBlankJti() { + assertFalse(tokenDenylist.isTokenDenylisted(" ")); + + verify(redisTemplate, never()).hasKey(anyString()); + } + + @Test + @DisplayName("isTokenDenylisted should fail open rather than block requests when Redis is down") + void isTokenDenylisted_shouldFailOpenWhenRedisIsDown() { + when(redisTemplate.hasKey(KEY)).thenThrow(new IllegalStateException("redis down")); + + assertFalse(tokenDenylist.isTokenDenylisted(JTI)); + } + } +} diff --git a/src/test/java/com/iemr/inventory/utils/UserAgentContextTest.java b/src/test/java/com/iemr/inventory/utils/UserAgentContextTest.java new file mode 100644 index 00000000..14e9e76c --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/UserAgentContextTest.java @@ -0,0 +1,88 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils; + +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DisplayName("UserAgentContext Test Suite") +class UserAgentContextTest { + + @AfterEach + @DisplayName("Clear the thread local after each test") + void tearDown() { + UserAgentContext.clear(); + } + + @Test + @DisplayName("getUserAgent should be empty before anything is set") + void getUserAgent_shouldBeEmptyByDefault() { + assertNull(UserAgentContext.getUserAgent()); + } + + @Test + @DisplayName("setUserAgent should make the value readable on the same thread") + void setUserAgent_shouldBeReadableOnSameThread() { + UserAgentContext.setUserAgent("okhttp/4.9.0"); + + assertEquals("okhttp/4.9.0", UserAgentContext.getUserAgent()); + } + + @Test + @DisplayName("setUserAgent should overwrite a previously stored value") + void setUserAgent_shouldOverwritePreviousValue() { + UserAgentContext.setUserAgent("okhttp/4.9.0"); + UserAgentContext.setUserAgent("Java/17.0.2"); + + assertEquals("Java/17.0.2", UserAgentContext.getUserAgent()); + } + + @Test + @DisplayName("clear should remove the stored value") + void clear_shouldRemoveStoredValue() { + UserAgentContext.setUserAgent("okhttp/4.9.0"); + + UserAgentContext.clear(); + + assertNull(UserAgentContext.getUserAgent()); + } + + @Test + @DisplayName("the stored value should not leak into another thread") + void storedValue_shouldNotLeakAcrossThreads() throws Exception { + UserAgentContext.setUserAgent("okhttp/4.9.0"); + ExecutorService executor = Executors.newSingleThreadExecutor(); + + Future otherThreadValue = executor.submit(UserAgentContext::getUserAgent); + + assertNull(otherThreadValue.get(), "the User-Agent is per-request, so must stay thread-confined"); + executor.shutdown(); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/config/ConfigPropertiesTest.java b/src/test/java/com/iemr/inventory/utils/config/ConfigPropertiesTest.java new file mode 100644 index 00000000..4f9f8604 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/config/ConfigPropertiesTest.java @@ -0,0 +1,169 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.config; + +import java.util.Base64; +import java.util.Properties; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("ConfigProperties Test Suite") +class ConfigPropertiesTest { + + private Properties originalProperties; + + @BeforeEach + @DisplayName("Instantiate the holder so application.properties is loaded, keeping the original statics") + void setUp() { + new ConfigProperties(); + originalProperties = (Properties) ReflectionTestUtils.getField(ConfigProperties.class, "properties"); + } + + @AfterEach + @DisplayName("Restore the shared static properties after each test") + void tearDown() { + ReflectionTestUtils.setField(ConfigProperties.class, "properties", originalProperties); + } + + @Nested + @DisplayName("Reading values from application.properties") + class PropertyLookupTests { + + @Test + @DisplayName("getPropertyByName should return the configured value for a known key") + void getPropertyByName_shouldReturnConfiguredValue() { + assertEquals("6379", ConfigProperties.getPropertyByName("spring.data.redis.port")); + } + + @Test + @DisplayName("getPropertyByName should return null for a key that is not configured") + void getPropertyByName_shouldReturnNullForUnknownKey() { + assertNull(ConfigProperties.getPropertyByName("no.such.key.configured")); + } + + @Test + @DisplayName("getBoolean should parse a boolean property") + void getBoolean_shouldParseBooleanProperty() { + assertTrue(ConfigProperties.getBoolean("iemr.extend.expiry.time")); + } + + @Test + @DisplayName("getBoolean should return false for a value that is not a boolean") + void getBoolean_shouldReturnFalseForNonBooleanValue() { + assertEquals(false, ConfigProperties.getBoolean("spring.data.redis.port")); + } + + @Test + @DisplayName("getInteger should parse an integer property") + void getInteger_shouldParseIntegerProperty() { + assertEquals(1800, ConfigProperties.getInteger("iemr.session.expiry.time")); + } + + @Test + @DisplayName("getInteger should fall back to zero when the value is not a number") + void getInteger_shouldFallBackToZeroForNonNumericValue() { + assertEquals(0, ConfigProperties.getInteger("spring.session.store-type")); + } + + @Test + @DisplayName("getLong should parse a long property") + void getLong_shouldParseLongProperty() { + assertEquals(1800L, new ConfigProperties().getLong("iemr.session.expiry.time")); + } + + @Test + @DisplayName("getLong should fall back to zero when the value is not a number") + void getLong_shouldFallBackToZeroForNonNumericValue() { + assertEquals(0L, new ConfigProperties().getLong("spring.session.store-type")); + } + + @Test + @DisplayName("getFloat should parse a numeric property") + void getFloat_shouldParseNumericProperty() { + assertEquals(6379F, new ConfigProperties().getFloat("spring.data.redis.port")); + } + + @Test + @DisplayName("getFloat should fall back to zero when the value is not a number") + void getFloat_shouldFallBackToZeroForNonNumericValue() { + assertEquals(0F, new ConfigProperties().getFloat("spring.session.store-type")); + } + } + + @Nested + @DisplayName("Session and Redis accessors") + class AccessorTests { + + @Test + @DisplayName("getSessionExpiryTime should resolve the configured session expiry") + void getSessionExpiryTime_shouldResolveConfiguredExpiry() { + assertEquals(1800, ConfigProperties.getSessionExpiryTime()); + } + + @Test + @DisplayName("getRedisPort should fall back to zero when no iemr.redis.port is configured") + void getRedisPort_shouldFallBackToZeroWhenUnconfigured() { + assertEquals(0, new ConfigProperties().getRedisPort()); + } + + @Test + @DisplayName("getRedisUrl should return null when no iemr.redis.url is configured") + void getRedisUrl_shouldReturnNullWhenUnconfigured() { + assertNull(new ConfigProperties().getRedisUrl()); + } + } + + @Nested + @DisplayName("Password handling") + class PasswordTests { + + @Test + @DisplayName("getPassword should return a plain-text password unchanged") + void getPassword_shouldReturnPlainTextUnchanged() { + Properties stub = new Properties(); + stub.setProperty("db.password", "plainSecret"); + ReflectionTestUtils.setField(ConfigProperties.class, "properties", stub); + + assertEquals("plainSecret", new ConfigProperties().getPassword("db.password")); + } + + @Test + @DisplayName("getPassword should Base64-decode a password tagged with the 0X10 prefix") + void getPassword_shouldBase64DecodeTaggedPassword() { + String encoded = Base64.getEncoder().encodeToString("s3cr3t".getBytes()); + Properties stub = new Properties(); + stub.setProperty("db.password", "0X10:" + encoded); + ReflectionTestUtils.setField(ConfigProperties.class, "properties", stub); + + assertEquals("s3cr3t", new ConfigProperties().getPassword("db.password")); + } + } +} diff --git a/src/test/java/com/iemr/inventory/utils/exception/CustomExceptionResponseTest.java b/src/test/java/com/iemr/inventory/utils/exception/CustomExceptionResponseTest.java new file mode 100644 index 00000000..4cf01e52 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/exception/CustomExceptionResponseTest.java @@ -0,0 +1,239 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.ConnectException; +import java.sql.SQLException; +import java.text.ParseException; + +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.DataException; +import org.hibernate.exception.GenericJDBCException; +import org.hibernate.exception.JDBCConnectionException; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.SQLGrammarException; +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +@DisplayName("CustomExceptionResponse Test Suite") +class CustomExceptionResponseTest { + + private CustomExceptionResponse response; + + @BeforeEach + @DisplayName("Start from a fresh response before each test") + void setUp() { + response = new CustomExceptionResponse(); + } + + /** setError(Throwable) branches on the cause, so every failure has to arrive wrapped. */ + private static RuntimeException wrapping(Throwable cause) { + return new RuntimeException("wrapper message", cause); + } + + @Nested + @DisplayName("Default state") + class DefaultStateTests { + + @Test + @DisplayName("a fresh response should read as a generic failure") + void freshResponse_shouldReadAsGenericFailure() { + assertEquals(CustomExceptionResponse.GENERIC_FAILURE, response.getStatusCode()); + assertEquals("Failed with generic error", response.getErrorMessage()); + assertEquals("FAILURE", response.getStatus()); + assertFalse(response.isSuccess()); + } + + @Test + @DisplayName("getData should return null while no payload has been set") + void getData_shouldReturnNullWithoutPayload() { + assertNull(response.getData()); + } + + @Test + @DisplayName("toStringWithSerialization should keep the null data field visible") + void toStringWithSerialization_shouldKeepNullData() { + assertTrue(response.toStringWithSerialization().contains("\"data\":null")); + } + } + + @Nested + @DisplayName("Successful payloads") + class SuccessPayloadTests { + + @Test + @DisplayName("setResponse should keep a JSON object payload as an object") + void setResponse_shouldKeepJsonObjectPayload() { + response.setResponse("{\"itemID\":11}"); + + assertTrue(response.isSuccess()); + assertEquals(CustomExceptionResponse.SUCCESS, response.getStatusCode()); + assertEquals("Success", response.getErrorMessage()); + assertEquals("{\"itemID\":11}", response.getData()); + } + + @Test + @DisplayName("setResponse should keep a JSON array payload as an array") + void setResponse_shouldKeepJsonArrayPayload() { + response.setResponse("[{\"itemID\":11}]"); + + assertTrue(response.getData().startsWith("[")); + } + + @Test + @DisplayName("setResponse should wrap a plain string payload in a response envelope") + void setResponse_shouldWrapPlainStringPayload() { + response.setResponse("all good"); + + assertTrue(response.getData().contains("all good")); + assertTrue(response.getData().contains("response")); + } + + @Test + @DisplayName("toString should serialise long values as strings") + void toString_shouldSerialiseLongsAsStrings() { + response.setResponse("{\"itemID\":11}"); + + assertTrue(response.toString().contains("\"statusCode\":200")); + } + } + + @Nested + @DisplayName("Explicit error codes") + class ExplicitErrorTests { + + @Test + @DisplayName("setError(code, message, status) should set all three fields") + void setError_shouldSetCodeMessageAndStatus() { + response.setError(CustomExceptionResponse.NOT_FOUND, "no such item", CustomExceptionResponse.NOT_FOUND_SC); + + assertEquals(404, response.getStatusCode()); + assertEquals("no such item", response.getErrorMessage()); + assertEquals("NOT_FOUND", response.getStatus()); + } + + @Test + @DisplayName("setError(code, message) should reuse the message as the status") + void setError_shouldReuseMessageAsStatus() { + response.setError(CustomExceptionResponse.BAD_REQUEST, "bad request"); + + assertEquals(400, response.getStatusCode()); + assertEquals("bad request", response.getStatus()); + } + } + + @Nested + @DisplayName("Mapping a thrown cause to a status code") + class CauseMappingTests { + + @Test + @DisplayName("an IEMRException cause should map to the user-id failure code") + void setError_shouldMapIemrException() { + response.setError(wrapping(new IEMRException("bad session"))); + + assertEquals(CustomExceptionResponse.USERID_FAILURE, response.getStatusCode()); + assertEquals("User login failed", response.getStatus()); + } + + @Test + @DisplayName("a JSONException cause should map to the object failure code") + void setError_shouldMapJsonException() { + response.setError(wrapping(new JSONException("bad json"))); + + assertEquals(CustomExceptionResponse.OBJECT_FAILURE, response.getStatusCode()); + assertEquals("Invalid object conversion", response.getErrorMessage()); + } + + @Test + @DisplayName("a plain SQLException cause should map to the DB exception code") + void setError_shouldMapSqlException() { + response.setError(wrapping(new SQLException("deadlock"))); + + assertEquals(CustomExceptionResponse.DB_EXCEPTION, response.getStatusCode()); + assertEquals(CustomExceptionResponse.DB_EXCEPTION_SC, response.getStatus()); + } + + @Test + @DisplayName("every Hibernate data-access cause should map to the DB exception code") + void setError_shouldMapHibernateCauses() { + SQLException sqlException = new SQLException("underlying"); + Throwable[] causes = { + new SQLGrammarException("bad grammar", sqlException), + new DataException("bad data", sqlException), + new ConstraintViolationException("duplicate key", sqlException, "uk_item"), + new GenericJDBCException("jdbc trouble", sqlException), + new JDBCConnectionException("connection lost", sqlException), + new LockAcquisitionException("lock timeout", sqlException), + new InvalidDataAccessResourceUsageException("bad resource use") + }; + + for (Throwable cause : causes) { + CustomExceptionResponse fresh = new CustomExceptionResponse(); + fresh.setError(wrapping(cause)); + + assertEquals(CustomExceptionResponse.DB_EXCEPTION, fresh.getStatusCode(), + () -> cause.getClass().getSimpleName() + " must map to the DB exception code"); + } + } + + @Test + @DisplayName("every environmental cause should map to the environment exception code") + void setError_shouldMapEnvironmentalCauses() { + Throwable[] causes = { + new ParseException("bad date", 0), + new NullPointerException("null field"), + new ArrayIndexOutOfBoundsException("index 5"), + new IOException("disk full"), + new ConnectException("refused") + }; + + for (Throwable cause : causes) { + CustomExceptionResponse fresh = new CustomExceptionResponse(); + fresh.setError(wrapping(cause)); + + assertEquals(CustomExceptionResponse.ENVIRONMENT_EXCEPTION, fresh.getStatusCode(), + () -> cause.getClass().getSimpleName() + " must map to the environment exception code"); + assertTrue(fresh.getStatus().startsWith("Failed with connection issues")); + } + } + + @Test + @DisplayName("an unrecognised cause should fall back to the generic failure code") + void setError_shouldFallBackToGenericFailure() { + response.setError(wrapping(new IllegalStateException("something else"))); + + assertEquals(CustomExceptionResponse.GENERIC_FAILURE, response.getStatusCode()); + assertEquals("wrapper message", response.getErrorMessage()); + assertTrue(response.getStatus().startsWith("Failed with wrapper message")); + } + } +} diff --git a/src/test/java/com/iemr/inventory/utils/exception/ExceptionsTest.java b/src/test/java/com/iemr/inventory/utils/exception/ExceptionsTest.java new file mode 100644 index 00000000..95fb57e7 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/exception/ExceptionsTest.java @@ -0,0 +1,114 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.exception; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DisplayName("AMRIT exception types Test Suite") +class ExceptionsTest { + + private static final String MESSAGE = "Invalid session key"; + + private RuntimeException causeWithStackTrace() { + RuntimeException cause = new RuntimeException("root cause"); + cause.setStackTrace(new StackTraceElement[] { + new StackTraceElement("com.iemr.Origin", "failingMethod", "Origin.java", 42) }); + return cause; + } + + @Nested + @DisplayName("IEMRException") + class IEMRExceptionTests { + + @Test + @DisplayName("the message constructor should expose the message through both accessors") + void messageConstructor_shouldExposeMessage() { + IEMRException exception = new IEMRException(MESSAGE); + + assertEquals(MESSAGE, exception.getMessage()); + assertEquals(MESSAGE, exception.toString()); + } + + @Test + @DisplayName("the cause constructor should adopt the stack trace of the cause") + void causeConstructor_shouldAdoptCauseStackTrace() { + RuntimeException cause = causeWithStackTrace(); + + IEMRException exception = new IEMRException(MESSAGE, cause); + + assertEquals(MESSAGE, exception.getMessage()); + assertArrayEquals(cause.getStackTrace(), exception.getStackTrace()); + } + + @Test + @DisplayName("the cause constructor should not chain the cause itself") + void causeConstructor_shouldNotChainCause() { + IEMRException exception = new IEMRException(MESSAGE, causeWithStackTrace()); + + assertNull(exception.getCause(), + "only the stack trace is adopted; the cause is deliberately not chained"); + } + + @Test + @DisplayName("toString should return null when constructed with a null message") + void toString_shouldReturnNullForNullMessage() { + assertNull(new IEMRException(null).toString()); + } + } + + @Nested + @DisplayName("InventoryException") + class InventoryExceptionTests { + + @Test + @DisplayName("the message constructor should expose the message through both accessors") + void messageConstructor_shouldExposeMessage() { + InventoryException exception = new InventoryException(MESSAGE); + + assertEquals(MESSAGE, exception.getMessage()); + assertEquals(MESSAGE, exception.toString()); + } + + @Test + @DisplayName("the cause constructor should adopt the stack trace of the cause") + void causeConstructor_shouldAdoptCauseStackTrace() { + RuntimeException cause = causeWithStackTrace(); + + InventoryException exception = new InventoryException(MESSAGE, cause); + + assertEquals(MESSAGE, exception.getMessage()); + assertArrayEquals(cause.getStackTrace(), exception.getStackTrace()); + } + + @Test + @DisplayName("the cause constructor should not chain the cause itself") + void causeConstructor_shouldNotChainCause() { + assertNull(new InventoryException(MESSAGE, causeWithStackTrace()).getCause()); + } + } +} diff --git a/src/test/java/com/iemr/inventory/utils/gateway/email/GenericEmailServiceImplTest.java b/src/test/java/com/iemr/inventory/utils/gateway/email/GenericEmailServiceImplTest.java new file mode 100644 index 00000000..b94e21a7 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/gateway/email/GenericEmailServiceImplTest.java @@ -0,0 +1,111 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.gateway.email; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import org.json.JSONException; +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; + +@ExtendWith(MockitoExtension.class) +@DisplayName("GenericEmailServiceImpl Test Suite") +class GenericEmailServiceImplTest { + + private static final String SINGLE_RECIPIENT = + "{\"to\":\"ops@example.org\",\"from\":\"inventory@example.org\"," + + "\"subject\":\"Low stock\",\"message\":\"Paracetamol is running low\"}"; + + @Mock + private JavaMailSender javaMailSender; + + private GenericEmailServiceImpl emailService; + + @BeforeEach + @DisplayName("Wire the service with a mocked mail sender") + void setUp() { + emailService = new GenericEmailServiceImpl(); + emailService.setJavaMailSender(javaMailSender); + } + + private SimpleMailMessage captureSentMessage() { + ArgumentCaptor captor = ArgumentCaptor.forClass(SimpleMailMessage.class); + verify(javaMailSender).send(captor.capture()); + return captor.getValue(); + } + + @Test + @DisplayName("sendEmail(payload, template) should send the single recipient described by the payload") + void sendEmailWithTemplate_shouldSendSingleRecipient() { + emailService.sendEmail(SINGLE_RECIPIENT, "low-stock-template"); + + SimpleMailMessage sent = captureSentMessage(); + assertArrayEquals(new String[] { "ops@example.org" }, sent.getTo()); + assertEquals("inventory@example.org", sent.getFrom()); + assertEquals("Low stock", sent.getSubject()); + assertEquals("Paracetamol is running low", sent.getText()); + } + + @Test + @DisplayName("sendEmail(payload) should split a semicolon-separated recipient list") + void sendEmail_shouldSplitRecipientList() { + emailService.sendEmail("{\"to\":\"ops@example.org;stores@example.org\"," + + "\"from\":\"inventory@example.org\",\"subject\":\"Low stock\",\"message\":\"Running low\"}"); + + assertArrayEquals(new String[] { "ops@example.org", "stores@example.org" }, captureSentMessage().getTo()); + } + + @Test + @DisplayName("sendEmail(payload) should send a lone recipient unchanged") + void sendEmail_shouldSendLoneRecipientUnchanged() { + emailService.sendEmail(SINGLE_RECIPIENT); + + assertArrayEquals(new String[] { "ops@example.org" }, captureSentMessage().getTo()); + } + + @Test + @DisplayName("sendEmail should refuse a payload that is missing a required field") + void sendEmail_shouldRefusePayloadMissingField() { + assertThrows(JSONException.class, () -> emailService.sendEmail("{\"to\":\"ops@example.org\"}")); + + verifyNoInteractions(javaMailSender); + } + + @Test + @DisplayName("sendEmailWithAttachment is not implemented and should send nothing") + void sendEmailWithAttachment_shouldSendNothing() { + emailService.sendEmailWithAttachment(SINGLE_RECIPIENT, "low-stock-template"); + + verifyNoInteractions(javaMailSender); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/http/AuthorizationHeaderRequestWrapperTest.java b/src/test/java/com/iemr/inventory/utils/http/AuthorizationHeaderRequestWrapperTest.java new file mode 100644 index 00000000..ad4a02a0 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/http/AuthorizationHeaderRequestWrapperTest.java @@ -0,0 +1,128 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.http; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("AuthorizationHeaderRequestWrapper Test Suite") +class AuthorizationHeaderRequestWrapperTest { + + private MockHttpServletRequest request; + + @BeforeEach + @DisplayName("Create a request carrying an inbound Authorization header before each test") + void setUp() { + request = new MockHttpServletRequest(); + request.addHeader("Authorization", "inbound-key"); + request.addHeader("JwtToken", "header-token"); + } + + @Test + @DisplayName("getHeader should return the overridden value for Authorization") + void getHeader_shouldReturnOverriddenAuthorization() { + AuthorizationHeaderRequestWrapper wrapper = + new AuthorizationHeaderRequestWrapper(request, "overridden-key"); + + assertEquals("overridden-key", wrapper.getHeader("Authorization")); + } + + @Test + @DisplayName("getHeader should match the Authorization name case-insensitively") + void getHeader_shouldMatchAuthorizationCaseInsensitively() { + AuthorizationHeaderRequestWrapper wrapper = + new AuthorizationHeaderRequestWrapper(request, "overridden-key"); + + assertEquals("overridden-key", wrapper.getHeader("authorization")); + assertEquals("overridden-key", wrapper.getHeader("AUTHORIZATION")); + } + + @Test + @DisplayName("getHeader should pass every other header through to the wrapped request") + void getHeader_shouldPassOtherHeadersThrough() { + AuthorizationHeaderRequestWrapper wrapper = + new AuthorizationHeaderRequestWrapper(request, "overridden-key"); + + assertEquals("header-token", wrapper.getHeader("JwtToken")); + assertNull(wrapper.getHeader("X-Not-Present")); + } + + @Test + @DisplayName("getHeader should return the blank override the JWT filter installs") + void getHeader_shouldReturnBlankOverride() { + AuthorizationHeaderRequestWrapper wrapper = new AuthorizationHeaderRequestWrapper(request, ""); + + assertEquals("", wrapper.getHeader("Authorization"), + "the filter blanks Authorization once the JWT has been validated"); + } + + @Test + @DisplayName("getHeaders should return the overridden Authorization as a single-valued enumeration") + void getHeaders_shouldReturnOverriddenAuthorizationAsSingleValue() { + AuthorizationHeaderRequestWrapper wrapper = + new AuthorizationHeaderRequestWrapper(request, "overridden-key"); + + assertEquals(List.of("overridden-key"), Collections.list(wrapper.getHeaders("Authorization"))); + } + + @Test + @DisplayName("getHeaders should pass every other header through to the wrapped request") + void getHeaders_shouldPassOtherHeadersThrough() { + AuthorizationHeaderRequestWrapper wrapper = + new AuthorizationHeaderRequestWrapper(request, "overridden-key"); + + assertEquals(List.of("header-token"), Collections.list(wrapper.getHeaders("JwtToken"))); + } + + @Test + @DisplayName("getHeaderNames should still list Authorization alongside the wrapped names") + void getHeaderNames_shouldListAuthorizationAlongsideWrappedNames() { + AuthorizationHeaderRequestWrapper wrapper = + new AuthorizationHeaderRequestWrapper(request, "overridden-key"); + + List names = Collections.list(wrapper.getHeaderNames()); + assertTrue(names.contains("Authorization")); + assertTrue(names.contains("JwtToken")); + assertEquals(1, names.stream().filter("Authorization"::equals).count(), + "Authorization must not be duplicated when the wrapped request already carries it"); + } + + @Test + @DisplayName("getHeaderNames should add Authorization when the wrapped request lacks it") + void getHeaderNames_shouldAddAuthorizationWhenWrappedRequestLacksIt() { + MockHttpServletRequest bare = new MockHttpServletRequest(); + bare.addHeader("JwtToken", "header-token"); + AuthorizationHeaderRequestWrapper wrapper = + new AuthorizationHeaderRequestWrapper(bare, "overridden-key"); + + assertTrue(Collections.list(wrapper.getHeaderNames()).contains("Authorization")); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/http/HTTPRequestInterceptorTest.java b/src/test/java/com/iemr/inventory/utils/http/HTTPRequestInterceptorTest.java new file mode 100644 index 00000000..cb59a9ac --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/http/HTTPRequestInterceptorTest.java @@ -0,0 +1,269 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.http; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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 org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.mock.web.DelegatingServletOutputStream; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.inventory.utils.redis.RedisStorage; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.ByteArrayOutputStream; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("HTTPRequestInterceptor Test Suite") +class HTTPRequestInterceptorTest { + + @Mock + private RedisStorage redisStorage; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + + private HTTPRequestInterceptor interceptor; + private ByteArrayOutputStream responseBody; + + @BeforeEach + @DisplayName("Wire the interceptor with a mocked Redis store and a capturing response stream") + void setUp() throws Exception { + interceptor = new HTTPRequestInterceptor(); + ReflectionTestUtils.setField(interceptor, "redisStorage", redisStorage); + ReflectionTestUtils.setField(interceptor, "allowedOrigins", "http://localhost:*,https://*.piramalswasthya.org"); + + responseBody = new ByteArrayOutputStream(); + when(response.getOutputStream()).thenReturn(new DelegatingServletOutputStream(responseBody)); + } + + @Test + @DisplayName("preHandle should let swagger-ui requests through without touching Redis") + void preHandle_shouldAllowSwaggerUi() throws Exception { + when(request.getRequestURI()).thenReturn("/inventory/swagger-ui/index.html"); + + assertTrue(interceptor.preHandle(request, response, new Object())); + verify(redisStorage, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should allow the request through when no Authorization header is supplied") + void preHandle_shouldAllowWhenAuthorizationMissing() throws Exception { + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn(null); + + assertTrue(interceptor.preHandle(request, response, new Object())); + verify(redisStorage, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should allow the request through when the Authorization header is empty") + void preHandle_shouldAllowWhenAuthorizationEmpty() throws Exception { + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn(""); + + assertTrue(interceptor.preHandle(request, response, new Object())); + } + + @Test + @DisplayName("preHandle should skip validation for OPTIONS pre-flight requests") + void preHandle_shouldSkipValidationForOptions() throws Exception { + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getMethod()).thenReturn("OPTIONS"); + + assertTrue(interceptor.preHandle(request, response, new Object())); + verify(redisStorage, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should strip the Bearer prefix before looking the session up in Redis") + void preHandle_shouldStripBearerPrefix() throws Exception { + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn("Bearer session-key"); + when(request.getMethod()).thenReturn("POST"); + when(redisStorage.getSessionObject("session-key")).thenReturn("{\"userName\":\"john\"}"); + + assertTrue(interceptor.preHandle(request, response, new Object())); + verify(redisStorage).getSessionObject("session-key"); + } + + @Test + @DisplayName("preHandle should accept a raw session key with no Bearer prefix") + void preHandle_shouldAcceptRawSessionKey() throws Exception { + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getMethod()).thenReturn("POST"); + when(redisStorage.getSessionObject("session-key")).thenReturn("{\"userName\":\"john\"}"); + + assertTrue(interceptor.preHandle(request, response, new Object())); + } + + @Test + @DisplayName("preHandle should reject the request and write an error payload when Redis has no session") + void preHandle_shouldRejectWhenSessionMissing() throws Exception { + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getMethod()).thenReturn("POST"); + when(redisStorage.getSessionObject("session-key")).thenReturn(null); + + assertFalse(interceptor.preHandle(request, response, new Object())); + assertTrue(responseBody.toString().contains("5002")); + } + + @Test + @DisplayName("preHandle should reject the request when Redis lookup blows up") + void preHandle_shouldRejectWhenRedisThrows() throws Exception { + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getMethod()).thenReturn("POST"); + when(redisStorage.getSessionObject("session-key")).thenThrow(new RuntimeException("redis down")); + + assertFalse(interceptor.preHandle(request, response, new Object())); + } + + @Test + @DisplayName("preHandle should reject requests routed to /error") + void preHandle_shouldRejectErrorEndpoint() throws Exception { + when(request.getRequestURI()).thenReturn("/error"); + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getMethod()).thenReturn("POST"); + + assertFalse(interceptor.preHandle(request, response, new Object())); + verify(redisStorage, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should let the documented swagger endpoints through untouched") + void preHandle_shouldAllowSwaggerRelatedEndpoints() throws Exception { + for (String uri : new String[] { "/swagger-ui.html", "/index.html", "/a/swagger-initializer.js", + "/v3/swagger-config", "/ui", "/swagger-resources", "/v3/api-docs" }) { + when(request.getRequestURI()).thenReturn(uri); + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getMethod()).thenReturn("GET"); + + assertTrue(interceptor.preHandle(request, response, new Object()), "expected " + uri + " to be allowed"); + } + verify(redisStorage, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should echo CORS headers back for an allowed origin on the error response") + void preHandle_shouldAddCorsHeadersForAllowedOrigin() throws Exception { + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getMethod()).thenReturn("POST"); + when(request.getHeader("Origin")).thenReturn("http://localhost:4200"); + when(redisStorage.getSessionObject("session-key")).thenReturn(null); + + assertFalse(interceptor.preHandle(request, response, new Object())); + verify(response).setHeader("Access-Control-Allow-Origin", "http://localhost:4200"); + verify(response).setHeader("Access-Control-Allow-Credentials", "true"); + } + + @Test + @DisplayName("preHandle should not add CORS headers for an origin outside the allow-list") + void preHandle_shouldNotAddCorsHeadersForDisallowedOrigin() throws Exception { + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getMethod()).thenReturn("POST"); + when(request.getHeader("Origin")).thenReturn("http://evil.example.com"); + when(redisStorage.getSessionObject("session-key")).thenReturn(null); + + assertFalse(interceptor.preHandle(request, response, new Object())); + verify(response, never()).setHeader(anyString(), anyString()); + } + + @Test + @DisplayName("preHandle should not add CORS headers when the allow-list is blank") + void preHandle_shouldNotAddCorsHeadersWhenAllowListBlank() throws Exception { + ReflectionTestUtils.setField(interceptor, "allowedOrigins", " "); + when(request.getRequestURI()).thenReturn("/getUom"); + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getMethod()).thenReturn("POST"); + when(request.getHeader("Origin")).thenReturn("http://localhost:4200"); + when(redisStorage.getSessionObject("session-key")).thenReturn(null); + + assertFalse(interceptor.preHandle(request, response, new Object())); + verify(response, never()).setHeader(anyString(), anyString()); + } + + @Test + @DisplayName("postHandle should refresh both the concurrent and the plain session entries") + void postHandle_shouldRefreshSession() throws Exception { + when(request.getHeader("Authorization")).thenReturn("Bearer session-key"); + when(request.getRequestURI()).thenReturn("/getUom"); + when(redisStorage.getSessionObject("session-key")).thenReturn("{\"userName\":\"john\"}"); + + interceptor.postHandle(request, response, new Object(), null); + + verify(redisStorage).updateConcurrentSessionObject("{\"userName\":\"john\"}"); + verify(redisStorage).updateSessionObject("session-key"); + } + + @Test + @DisplayName("postHandle should do nothing when there is no Authorization header") + void postHandle_shouldSkipWhenAuthorizationMissing() throws Exception { + when(request.getHeader("Authorization")).thenReturn(null); + when(request.getRequestURI()).thenReturn("/getUom"); + + interceptor.postHandle(request, response, new Object(), null); + + verify(redisStorage, never()).updateSessionObject(anyString()); + } + + @Test + @DisplayName("postHandle should swallow Redis failures rather than propagating them") + void postHandle_shouldSwallowRedisFailures() throws Exception { + when(request.getHeader("Authorization")).thenReturn("session-key"); + when(request.getRequestURI()).thenReturn("/getUom"); + when(redisStorage.getSessionObject("session-key")).thenThrow(new RuntimeException("redis down")); + + interceptor.postHandle(request, response, new Object(), null); + + verify(redisStorage, times(1)).getSessionObject("session-key"); + } + + @Test + @DisplayName("afterCompletion should complete without side effects") + void afterCompletion_shouldDoNothing() throws Exception { + interceptor.afterCompletion(request, response, new Object(), null); + verify(redisStorage, never()).updateSessionObject(anyString()); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/http/HttpUtilsTest.java b/src/test/java/com/iemr/inventory/utils/http/HttpUtilsTest.java new file mode 100644 index 00000000..f9289426 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/http/HttpUtilsTest.java @@ -0,0 +1,286 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.http; + +import java.util.HashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("HttpUtils Test Suite") +class HttpUtilsTest { + + private static final String URI = "http://localhost:8080/api/resource"; + + @Mock + private RestTemplate restTemplate; + + private HttpUtils httpUtils; + + @BeforeEach + @DisplayName("Replace the internal RestTemplate with a mock before each test") + void setUp() { + httpUtils = new HttpUtils(); + ReflectionTestUtils.setField(httpUtils, "rest", restTemplate); + } + + @SuppressWarnings("unchecked") + private ArgumentCaptor> captureRequest(HttpMethod method, ResponseEntity reply) { + ArgumentCaptor> captor = ArgumentCaptor.forClass(HttpEntity.class); + when(restTemplate.exchange(eq(URI), eq(method), captor.capture(), eq(String.class))).thenReturn(reply); + return captor; + } + + @Nested + @DisplayName("GET requests") + class GetTests + + { + @Test + @DisplayName("get should return the response body and record the status") + void get_shouldReturnBodyAndRecordStatus() { + when(restTemplate.exchange(eq(URI), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("{\"ok\":true}", HttpStatus.OK)); + + assertEquals("{\"ok\":true}", httpUtils.get(URI)); + assertEquals(HttpStatus.OK, httpUtils.getStatus()); + } + + @Test + @DisplayName("get should send the default JSON content type") + void get_shouldSendDefaultJsonContentType() { + ArgumentCaptor> captor = + captureRequest(HttpMethod.GET, new ResponseEntity<>("body", HttpStatus.OK)); + + httpUtils.get(URI); + + assertEquals("application/json", + captor.getValue().getHeaders().getFirst("Content-Type")); + } + + @Test + @DisplayName("get should record a non-OK status returned by the server") + void get_shouldRecordNonOkStatus() { + when(restTemplate.exchange(eq(URI), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>(null, HttpStatus.NOT_FOUND)); + + assertNull(httpUtils.get(URI)); + assertEquals(HttpStatus.NOT_FOUND, httpUtils.getStatus()); + } + + @Test + @DisplayName("get with headers should forward the supplied Authorization header") + void get_shouldForwardSuppliedAuthorizationHeader() { + HashMap header = new HashMap<>(); + header.put(HttpHeaders.AUTHORIZATION, "session-key-123"); + ArgumentCaptor> captor = + captureRequest(HttpMethod.GET, new ResponseEntity<>("body", HttpStatus.OK)); + + assertEquals("body", httpUtils.get(URI, header)); + assertEquals("session-key-123", + captor.getValue().getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + @DisplayName("get with headers should forward an explicit Content-Type") + void get_shouldForwardExplicitContentType() { + HashMap header = new HashMap<>(); + header.put(HttpHeaders.CONTENT_TYPE, "application/xml"); + ArgumentCaptor> captor = + captureRequest(HttpMethod.GET, new ResponseEntity<>("body", HttpStatus.OK)); + + httpUtils.get(URI, header); + + assertEquals("application/xml", + captor.getValue().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)); + } + + @Test + @DisplayName("get with headers should default the Content-Type to JSON when none is supplied") + void get_shouldDefaultContentTypeToJson() { + ArgumentCaptor> captor = + captureRequest(HttpMethod.GET, new ResponseEntity<>("body", HttpStatus.OK)); + + httpUtils.get(URI, new HashMap<>()); + + assertEquals("application/json", + captor.getValue().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)); + } + + @Test + @DisplayName("get should propagate a transport failure to the caller") + void get_shouldPropagateTransportFailure() { + when(restTemplate.exchange(eq(URI), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))) + .thenThrow(new RestClientException("connection refused")); + + assertThrows(RestClientException.class, () -> httpUtils.get(URI)); + } + } + + @Nested + @DisplayName("POST requests") + class PostTests { + + @Test + @DisplayName("post should send the JSON payload and return the response body") + void post_shouldSendPayloadAndReturnBody() { + ArgumentCaptor> captor = + captureRequest(HttpMethod.POST, new ResponseEntity<>("created", HttpStatus.CREATED)); + + assertEquals("created", httpUtils.post(URI, "{\"count\":5}")); + assertEquals("{\"count\":5}", captor.getValue().getBody()); + assertEquals(HttpStatus.CREATED, httpUtils.getStatus()); + } + + @Test + @DisplayName("post with headers should forward the supplied Authorization header") + void post_shouldForwardSuppliedAuthorizationHeader() { + HashMap header = new HashMap<>(); + header.put(HttpHeaders.AUTHORIZATION, "session-key-123"); + ArgumentCaptor> captor = + captureRequest(HttpMethod.POST, new ResponseEntity<>("created", HttpStatus.CREATED)); + + assertEquals("created", httpUtils.post(URI, "{\"count\":5}", header)); + assertEquals("session-key-123", + captor.getValue().getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + assertEquals("{\"count\":5}", captor.getValue().getBody()); + } + + @Test + @DisplayName("post with headers should omit the Authorization header when none is supplied") + void post_shouldOmitAuthorizationHeaderWhenNoneSupplied() { + ArgumentCaptor> captor = + captureRequest(HttpMethod.POST, new ResponseEntity<>("created", HttpStatus.CREATED)); + + httpUtils.post(URI, "{}", new HashMap<>()); + + assertNull(captor.getValue().getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + } + + @Test + @DisplayName("post should record a server error status") + void post_shouldRecordServerErrorStatus() { + when(restTemplate.exchange(eq(URI), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("boom", HttpStatus.INTERNAL_SERVER_ERROR)); + + httpUtils.post(URI, "{}"); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, httpUtils.getStatus()); + } + + @Test + @DisplayName("post should issue the request against the supplied URI with the POST method") + void post_shouldIssueRequestWithPostMethod() { + when(restTemplate.exchange(eq(URI), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("created", HttpStatus.CREATED)); + + httpUtils.post(URI, "{}"); + + verify(restTemplate).exchange(eq(URI), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)); + } + } + + @Nested + @DisplayName("Status tracking") + class StatusTests { + + @Test + @DisplayName("getStatus should be null until a request has been made") + void getStatus_shouldBeNullBeforeAnyRequest() { + assertNull(httpUtils.getStatus()); + } + + @Test + @DisplayName("setStatus should record the supplied status code") + void setStatus_shouldRecordSuppliedStatusCode() { + httpUtils.setStatus(HttpStatus.ACCEPTED); + + assertEquals(HttpStatus.ACCEPTED, httpUtils.getStatus()); + } + } + + @Nested + @DisplayName("uploadFile") + class UploadFileTests { + + @Test + @DisplayName("uploadFile should post the payload as a plain body when the content type is not multipart") + void uploadFile_shouldPostPlainBodyForNonMultipart() throws Exception { + HashMap header = new HashMap<>(); + header.put(HttpHeaders.AUTHORIZATION, "session-key"); + header.put(HttpHeaders.CONTENT_TYPE, "application/json"); + when(restTemplate.exchange(eq(URI), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("{\"uploaded\":true}", HttpStatus.OK)); + + assertEquals("{\"uploaded\":true}", httpUtils.uploadFile(URI, "{\"docPath\":\"/tmp/a.pdf\"}", header)); + assertEquals(HttpStatus.OK, httpUtils.getStatus()); + } + + @Test + @DisplayName("uploadFile should default the content type to JSON when the caller supplies none") + void uploadFile_shouldDefaultContentTypeToJson() throws Exception { + HashMap header = new HashMap<>(); + when(restTemplate.exchange(eq(URI), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("ok", HttpStatus.OK)); + + httpUtils.uploadFile(URI, "payload", header); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(HttpEntity.class); + verify(restTemplate).exchange(eq(URI), eq(HttpMethod.POST), captor.capture(), eq(String.class)); + assertEquals("application/json", + captor.getValue().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)); + } + + @Test + @DisplayName("uploadFile should swallow a missing file and still report the failed status") + void uploadFile_shouldSwallowMissingFile() throws Exception { + HashMap header = new HashMap<>(); + header.put(HttpHeaders.CONTENT_TYPE, "multipart/form-data"); + + assertNull(httpUtils.uploadFile(URI, "/no/such/file.pdf", header)); + assertEquals(HttpStatus.BAD_REQUEST, httpUtils.getStatus()); + } + } +} diff --git a/src/test/java/com/iemr/inventory/utils/mapper/InputMapperTest.java b/src/test/java/com/iemr/inventory/utils/mapper/InputMapperTest.java new file mode 100644 index 00000000..5c4dd0c4 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/mapper/InputMapperTest.java @@ -0,0 +1,133 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.mapper; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import com.google.gson.JsonSyntaxException; + +@DisplayName("InputMapper Test Suite") +class InputMapperTest { + + static class TestPojo { + String name; + int value; + Date date; + + public String getName() { return name; } + public int getValue() { return value; } + public Date getDate() { return date; } + } + + @Test + @DisplayName("Should return valid InputMapper instance from gson factory method") + void testGsonStaticFactoryMethod() { + InputMapper mapper = InputMapper.gson(); + assertNotNull(mapper); + assertTrue(mapper instanceof InputMapper); + } + + @Test + @DisplayName("Should successfully parse valid JSON to object") + void testFromJson_validJson() { + InputMapper mapper = InputMapper.gson(); + String json = "{\"name\":\"testName\", \"value\":100}"; + + TestPojo result = mapper.fromJson(json, TestPojo.class); + + assertNotNull(result); + assertEquals("testName", result.getName()); + assertEquals(100, result.getValue()); + assertNull(result.getDate()); + } + + @Test + @DisplayName("Should successfully parse JSON with date field") + void testFromJson_validJsonWithDate() throws ParseException { + InputMapper mapper = InputMapper.gson(); + String json = "{\"name\":\"itemWithDate\", \"value\":200, \"date\":\"2023-10-26T10:30:45.123\"}"; + + TestPojo result = mapper.fromJson(json, TestPojo.class); + + assertNotNull(result); + assertEquals("itemWithDate", result.getName()); + assertEquals(200, result.getValue()); + assertNotNull(result.getDate()); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + Date expectedDate = sdf.parse("2023-10-26T10:30:45.123"); + + assertEquals(expectedDate.getTime(), result.getDate().getTime()); + } + + @Test + @DisplayName("Should return null when JSON input is null") + void testFromJson_nullJson() { + InputMapper mapper = InputMapper.gson(); + String json = null; + + TestPojo result = mapper.fromJson(json, TestPojo.class); + assertNull(result); + } + + @Test + @DisplayName("Should return null when JSON input is empty string") + void testFromJson_emptyJsonString() { + InputMapper mapper = InputMapper.gson(); + String json = ""; + + // InputMapper's fromJson method (likely catching JsonSyntaxException internally) + // returns null when given an empty string. + TestPojo result = mapper.fromJson(json, TestPojo.class); + assertNull(result); + } + + @Test + @DisplayName("Should create object with default values when JSON is empty object") + void testFromJson_emptyJsonObject() { + InputMapper mapper = InputMapper.gson(); + String json = "{}"; + + TestPojo result = mapper.fromJson(json, TestPojo.class); + + assertNotNull(result); + assertNull(result.getName()); + assertEquals(0, result.getValue()); + assertNull(result.getDate()); + } + + @Test + @DisplayName("Should throw JsonSyntaxException when JSON is malformed") + void testFromJson_malformedJson() { + InputMapper mapper = InputMapper.gson(); + String json = "{\"name\":\"testName\", \"value\":,}"; + + JsonSyntaxException thrown = assertThrows(JsonSyntaxException.class, () -> mapper.fromJson(json, TestPojo.class)); + assertNotNull(thrown); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/redis/RedisStorageTest.java b/src/test/java/com/iemr/inventory/utils/redis/RedisStorageTest.java new file mode 100644 index 00000000..c6763598 --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/redis/RedisStorageTest.java @@ -0,0 +1,157 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.redis; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; + +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 org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("RedisStorage Test Suite") +class RedisStorageTest { + + @Mock + private LettuceConnectionFactory connectionFactory; + @Mock + private RedisConnection redisConnection; + @Mock + private RedisStringCommands stringCommands; + + private RedisStorage redisStorage; + + @BeforeEach + @DisplayName("Wire the store with a mocked Lettuce connection") + void setUp() { + redisStorage = new RedisStorage(); + ReflectionTestUtils.setField(redisStorage, "connection", connectionFactory); + ReflectionTestUtils.setField(redisStorage, "sessionExpiryTimeInSec", 1800); + + when(connectionFactory.getConnection()).thenReturn(redisConnection); + when(redisConnection.stringCommands()).thenReturn(stringCommands); + } + + private void storedValue(String key, String value) { + when(stringCommands.get(key.getBytes())) + .thenReturn(value == null ? null : value.getBytes(StandardCharsets.UTF_8)); + } + + @Test + @DisplayName("getSessionObject should return the stored session payload") + void getSessionObject_shouldReturnStoredPayload() throws Exception { + storedValue("session-key", "{\"userName\":\"john\"}"); + + assertEquals("{\"userName\":\"john\"}", redisStorage.getSessionObject("session-key")); + } + + @Test + @DisplayName("getSessionObject should throw when the key is absent from Redis") + void getSessionObject_shouldThrowWhenKeyAbsent() { + storedValue("session-key", null); + + Exception ex = assertThrows(Exception.class, () -> redisStorage.getSessionObject("session-key")); + assertEquals("Unable to fetch session object from Redis server," + + "either session key is invalid or expired.", ex.getMessage()); + } + + @Test + @DisplayName("getSessionObject should throw when the stored payload is blank") + void getSessionObject_shouldThrowWhenPayloadBlank() { + storedValue("session-key", " "); + + assertThrows(Exception.class, () -> redisStorage.getSessionObject("session-key")); + } + + @Test + @DisplayName("updateSessionObject should refresh the TTL and echo the key back") + void updateSessionObject_shouldRefreshTtl() throws Exception { + storedValue("session-key", "{\"userName\":\"john\"}"); + + assertEquals("session-key", redisStorage.updateSessionObject("session-key")); + verify(stringCommands).set(eq("session-key".getBytes()), any(byte[].class), + eq(Expiration.seconds(1800)), eq(SetOption.UPSERT)); + } + + @Test + @DisplayName("updateSessionObject should throw when the session is missing") + void updateSessionObject_shouldThrowWhenSessionMissing() { + storedValue("session-key", null); + + Exception ex = assertThrows(Exception.class, () -> redisStorage.updateSessionObject("session-key")); + assertEquals("Unable to fetch session object from Redis server", ex.getMessage()); + } + + @Test + @DisplayName("updateConcurrentSessionObject should refresh the lower-cased username key") + void updateConcurrentSessionObject_shouldRefreshUsernameKey() { + storedValue("john", "{\"userName\":\"John\"}"); + + redisStorage.updateConcurrentSessionObject("{\"userName\":\" John \"}"); + + verify(stringCommands).set(eq("john".getBytes()), any(byte[].class), any(Expiration.class), any(SetOption.class)); + } + + @Test + @DisplayName("updateConcurrentSessionObject should swallow malformed JSON") + void updateConcurrentSessionObject_shouldSwallowMalformedJson() { + redisStorage.updateConcurrentSessionObject("not-json-at-all"); + + verify(stringCommands, never()).set(any(), any(), any(), any()); + } + + @Test + @DisplayName("updateConcurrentSessionObject should do nothing when the payload has no userName") + void updateConcurrentSessionObject_shouldIgnorePayloadWithoutUserName() { + redisStorage.updateConcurrentSessionObject("{\"other\":\"value\"}"); + + verify(stringCommands, never()).set(any(), any(), any(), any()); + } + + @Test + @DisplayName("updateConcurrentSessionObject should swallow a null payload") + void updateConcurrentSessionObject_shouldSwallowNullPayload() { + redisStorage.updateConcurrentSessionObject(null); + + verify(stringCommands, never()).set(any(), any(), any(), any()); + } +} diff --git a/src/test/java/com/iemr/inventory/utils/response/OutputResponseTest.java b/src/test/java/com/iemr/inventory/utils/response/OutputResponseTest.java new file mode 100644 index 00000000..8ce93a7e --- /dev/null +++ b/src/test/java/com/iemr/inventory/utils/response/OutputResponseTest.java @@ -0,0 +1,299 @@ +/* +* AMRIT - Accessible Medical Records via Integrated Technologies +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.inventory.utils.response; + +import java.io.IOException; +import java.net.ConnectException; +import java.sql.SQLException; +import java.text.ParseException; + +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.iemr.inventory.utils.exception.IEMRException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("OutputResponse (response package) Test Suite") +class OutputResponseTest { + + private OutputResponse outputResponse; + + @BeforeEach + @DisplayName("Create a fresh response object before each test") + void setUp() { + outputResponse = new OutputResponse(); + } + + @Nested + @DisplayName("Default state") + class DefaultStateTests { + + @Test + @DisplayName("a new response should default to a generic failure") + void newResponse_shouldDefaultToGenericFailure() { + assertEquals(OutputResponse.GENERIC_FAILURE, outputResponse.getStatusCode()); + assertEquals("Failed with generic error", outputResponse.getErrorMessage()); + assertEquals("FAILURE", outputResponse.getStatus()); + assertFalse(outputResponse.isSuccess()); + } + + @Test + @DisplayName("getData should return null when no data has been set") + void getData_shouldReturnNullWhenNoDataSet() { + assertNull(outputResponse.getData()); + } + } + + @Nested + @DisplayName("setResponse") + class SetResponseTests { + + @Test + @DisplayName("setResponse should mark the response successful") + void setResponse_shouldMarkResponseSuccessful() { + outputResponse.setResponse("done"); + + assertEquals(OutputResponse.SUCCESS, outputResponse.getStatusCode()); + assertEquals("Success", outputResponse.getErrorMessage()); + assertEquals("Success", outputResponse.getStatus()); + assertTrue(outputResponse.isSuccess()); + } + + @Test + @DisplayName("setResponse should keep a JSON object payload as an object") + void setResponse_shouldKeepJsonObjectPayload() { + outputResponse.setResponse("{\"beneficiaryId\":12345}"); + + assertTrue(outputResponse.getData().contains("\"beneficiaryId\"")); + assertTrue(outputResponse.getData().startsWith("{")); + } + + @Test + @DisplayName("setResponse should keep a JSON array payload as an array") + void setResponse_shouldKeepJsonArrayPayload() { + outputResponse.setResponse("[1,2,3]"); + + assertTrue(outputResponse.getData().startsWith("[")); + assertTrue(outputResponse.getData().contains("1")); + } + + @Test + @DisplayName("setResponse should wrap a plain string payload under a response key") + void setResponse_shouldWrapPlainStringPayload() { + outputResponse.setResponse("plain text"); + + assertTrue(outputResponse.getData().contains("response")); + assertTrue(outputResponse.getData().contains("plain text")); + } + + @Test + @DisplayName("toString should serialise the exposed fields as JSON") + void toString_shouldSerialiseExposedFields() { + outputResponse.setResponse("done"); + + String json = outputResponse.toString(); + + assertTrue(json.contains("\"statusCode\":200")); + assertTrue(json.contains("\"status\":\"Success\"")); + assertTrue(json.contains("\"errorMessage\":\"Success\"")); + assertTrue(json.contains("\"data\"")); + } + + @Test + @DisplayName("toString should omit null fields while toStringWithSerialization keeps them") + void toString_shouldOmitNullsUnlikeToStringWithSerialization() { + assertFalse(outputResponse.toString().contains("\"data\"")); + assertTrue(outputResponse.toStringWithSerialization().contains("\"data\":null")); + } + } + + @Nested + @DisplayName("setError with an explicit code") + class SetErrorWithCodeTests { + + @Test + @DisplayName("setError should apply the supplied code, message and status") + void setError_shouldApplySuppliedCodeMessageAndStatus() { + outputResponse.setError(OutputResponse.PREVILAGE_FAILURE, "not permitted", "PRIVILEGE"); + + assertEquals(OutputResponse.PREVILAGE_FAILURE, outputResponse.getStatusCode()); + assertEquals("not permitted", outputResponse.getErrorMessage()); + assertEquals("PRIVILEGE", outputResponse.getStatus()); + assertFalse(outputResponse.isSuccess()); + } + + @Test + @DisplayName("setError should reuse the message as the status when only a message is supplied") + void setError_shouldReuseMessageAsStatus() { + outputResponse.setError(OutputResponse.PASSWORD_FAILURE, "bad password"); + + assertEquals(OutputResponse.PASSWORD_FAILURE, outputResponse.getStatusCode()); + assertEquals("bad password", outputResponse.getErrorMessage()); + assertEquals("bad password", outputResponse.getStatus()); + } + } + + @Nested + @DisplayName("setError mapped from a throwable") + class SetErrorFromThrowableTests { + + @Test + @DisplayName("setError should map IEMRException to a user login failure") + void setError_shouldMapIemrExceptionToUserIdFailure() { + outputResponse.setError(new IEMRException("invalid credentials")); + + assertEquals(OutputResponse.USERID_FAILURE, outputResponse.getStatusCode()); + assertEquals("User login failed", outputResponse.getStatus()); + assertEquals("invalid credentials", outputResponse.getErrorMessage()); + } + + @Test + @DisplayName("setError should map JSONException to an object conversion failure") + void setError_shouldMapJsonExceptionToObjectFailure() { + outputResponse.setError(new JSONException("bad json")); + + assertEquals(OutputResponse.OBJECT_FAILURE, outputResponse.getStatusCode()); + assertEquals("Invalid object conversion", outputResponse.getStatus()); + assertEquals("Invalid object conversion", outputResponse.getErrorMessage()); + } + + @Test + @DisplayName("setError should map SQLException to a code exception") + void setError_shouldMapSqlExceptionToCodeException() { + outputResponse.setError(new SQLException("deadlock")); + + assertEquals(OutputResponse.CODE_EXCEPTION, outputResponse.getStatusCode()); + assertTrue(outputResponse.getStatus().startsWith("Failed with critical errors at ")); + assertEquals("deadlock", outputResponse.getErrorMessage()); + } + + @Test + @DisplayName("setError should map NullPointerException to a code exception") + void setError_shouldMapNullPointerExceptionToCodeException() { + outputResponse.setError(new NullPointerException("npe")); + + assertEquals(OutputResponse.CODE_EXCEPTION, outputResponse.getStatusCode()); + } + + @Test + @DisplayName("setError should map ParseException to a code exception") + void setError_shouldMapParseExceptionToCodeException() { + outputResponse.setError(new ParseException("bad date", 0)); + + assertEquals(OutputResponse.CODE_EXCEPTION, outputResponse.getStatusCode()); + } + + @Test + @DisplayName("setError should map ArrayIndexOutOfBoundsException to a code exception") + void setError_shouldMapArrayIndexExceptionToCodeException() { + outputResponse.setError(new ArrayIndexOutOfBoundsException("index 5")); + + assertEquals(OutputResponse.CODE_EXCEPTION, outputResponse.getStatusCode()); + } + + @Test + @DisplayName("setError should map IOException to an environment exception") + void setError_shouldMapIoExceptionToEnvironmentException() { + outputResponse.setError(new IOException("disk full")); + + assertEquals(OutputResponse.ENVIRONMENT_EXCEPTION, outputResponse.getStatusCode()); + assertTrue(outputResponse.getStatus().startsWith("Failed with connection issues at ")); + assertEquals("disk full", outputResponse.getErrorMessage()); + } + + @Test + @DisplayName("setError should map ConnectException to an environment exception") + void setError_shouldMapConnectExceptionToEnvironmentException() { + outputResponse.setError(new ConnectException("refused")); + + assertEquals(OutputResponse.ENVIRONMENT_EXCEPTION, outputResponse.getStatusCode()); + } + + @Test + @DisplayName("setError should fall back to a generic failure for an unmapped exception") + void setError_shouldFallBackToGenericFailureForUnmappedException() { + outputResponse.setError(new IllegalStateException("something odd")); + + assertEquals(OutputResponse.GENERIC_FAILURE, outputResponse.getStatusCode()); + assertTrue(outputResponse.getStatus().startsWith("Failed with something odd at ")); + assertEquals("something odd", outputResponse.getErrorMessage()); + } + } + + @Nested + @DisplayName("Error mapping for exception types raised by other AMRIT modules") + class ExternalExceptionMappingTests { + + // setError switches on getClass().getSimpleName(), so locally declared types with + // the same simple names reach the arms meant for Hibernate/JDBC exceptions. + private static class JDBCException extends Exception { + JDBCException(String message) { + super(message); + } + } + + private static class SQLGrammarException extends Exception { + SQLGrammarException(String message) { + super(message); + } + } + + private static class ConstraintViolationException extends Exception { + ConstraintViolationException(String message) { + super(message); + } + } + + @Test + @DisplayName("setError should map a JDBC failure to a DB connection environment error") + void setError_shouldMapJdbcFailure() { + outputResponse.setError(new JDBCException("pool exhausted")); + + assertEquals(OutputResponse.ENVIRONMENT_EXCEPTION, outputResponse.getStatusCode()); + assertTrue(outputResponse.getStatus().startsWith("Failed with DB connection issues at ")); + assertEquals("pool exhausted", outputResponse.getErrorMessage()); + } + + @Test + @DisplayName("setError should map a SQL grammar failure to a code exception") + void setError_shouldMapSqlGrammarFailure() { + outputResponse.setError(new SQLGrammarException("bad column")); + + assertEquals(OutputResponse.CODE_EXCEPTION, outputResponse.getStatusCode()); + } + + @Test + @DisplayName("setError should fall back to the generic failure code for an unmapped constraint violation") + void setError_shouldMapConstraintViolation() { + outputResponse.setError(new ConstraintViolationException("duplicate key")); + + assertEquals(OutputResponse.GENERIC_FAILURE, outputResponse.getStatusCode()); + } + } +}