From d07422d25ae08f7fcf2619a9a5aa46ab13db66e7 Mon Sep 17 00:00:00 2001 From: Sneha Date: Wed, 1 Jul 2026 17:04:01 +0530 Subject: [PATCH 1/8] fix: amm-2368 item to shop mapping issue --- .../itemfacilitymapping/MItemFacilityMappingController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingController.java b/src/main/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingController.java index 0bf5bd5..c0f662c 100644 --- a/src/main/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingController.java +++ b/src/main/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingController.java @@ -93,6 +93,7 @@ public String mapItemtoStrore(@RequestBody String mapItemtoStrore) { resList.add(resDataMap); } + itemDetailsIndex++; } ArrayList data = M_itemfacilitymappingInter.mapItemtoStore(resList); From 931c56b2b7fefc6f4d0df70cb7daa3ab4605832f Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:57:43 +0530 Subject: [PATCH 2/8] Fix the Health Response Format (#138) * fix: health / version response format * chore: remove unused VersionService after /version rewrite VersionController now reads git.properties directly, so the old build-info- based VersionService is dead code. Co-Authored-By: Claude Opus 4.8 (1M context) * fix: force JSON response for /health endpoint Without an explicit produces type, Spring content-negotiated /health to XML when the browser's Accept header preferred it (Jackson XML is on the classpath), rendering the raw XML tree in-browser. Pin the endpoint to application/json. Co-Authored-By: Claude Opus 4.8 (1M context) * fix: pom version --------- Co-authored-by: Claude Opus 4.8 (1M context) --- Dockerfile | 2 +- pom.xml | 2 +- .../controller/health/HealthController.java | 7 +- .../controller/version/VersionController.java | 25 +- .../admin/service/health/HealthService.java | 778 ++++++++---------- .../admin/service/version/VersionService.java | 170 ---- 6 files changed, 354 insertions(+), 630 deletions(-) delete mode 100644 src/main/java/com/iemr/admin/service/version/VersionService.java diff --git a/Dockerfile b/Dockerfile index f99697f..9073787 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ COPY . . # Build the application while caching Maven dependencies to speed up future builds RUN --mount=type=cache,target=/root/.m2 \ - mvn clean package -DENV_VAR=docker -DskipTests -Dgit.skip=true + mvn clean package -DENV_VAR=docker -DskipTests # --- Stage 2: Run the application with a minimal JRE image --- FROM eclipse-temurin:17-jre diff --git a/pom.xml b/pom.xml index 4f180be..062df58 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.admin admin-api - 3.8.1 + 3.8.2 war Admin-API Admin Page diff --git a/src/main/java/com/iemr/admin/controller/health/HealthController.java b/src/main/java/com/iemr/admin/controller/health/HealthController.java index d1a9e80..3fa49ca 100644 --- a/src/main/java/com/iemr/admin/controller/health/HealthController.java +++ b/src/main/java/com/iemr/admin/controller/health/HealthController.java @@ -27,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -52,8 +53,8 @@ public HealthController(HealthService healthService) { this.healthService = healthService; } - @GetMapping - @Operation(summary = "Check infrastructure health", + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Check infrastructure health", description = "Returns the health status of MySQL, Redis, and other configured services") @ApiResponses({ @ApiResponse(responseCode = "200", description = "Services are UP or DEGRADED (operational with warnings)"), @@ -61,7 +62,7 @@ public HealthController(HealthService healthService) { }) public ResponseEntity> checkHealth() { logger.debug("Health check endpoint called"); - + try { Map healthStatus = healthService.checkHealth(); String overallStatus = (String) healthStatus.get("status"); diff --git a/src/main/java/com/iemr/admin/controller/version/VersionController.java b/src/main/java/com/iemr/admin/controller/version/VersionController.java index 15d14bb..387e861 100644 --- a/src/main/java/com/iemr/admin/controller/version/VersionController.java +++ b/src/main/java/com/iemr/admin/controller/version/VersionController.java @@ -1,8 +1,8 @@ /* -* AMRIT – Accessible Medical Records via Integrated Technology -* Integrated EHR (Electronic Health Records) Solution +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution * -* Copyright (C) "Piramal Swasthya Management and Research Institute" +* Copyright (C) "Piramal Swasthya Management and Research Institute" * * This file is part of AMRIT. * @@ -22,26 +22,29 @@ package com.iemr.admin.controller.version; +import java.io.InputStream; +import java.io.IOException; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Properties; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; + import io.swagger.v3.oas.annotations.Operation; -import java.io.IOException; -import java.io.InputStream; -import java.util.LinkedHashMap; -import java.util.Properties; -import org.springframework.http.MediaType; @RestController public class VersionController { - private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); - + private final Logger logger = + LoggerFactory.getLogger(this.getClass().getSimpleName()); + private static final String UNKNOWN_VALUE = "unknown"; - + @Operation(summary = "Get version information") @GetMapping(value = "/version", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> versionInformation() { diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index e6a0432..b679cd1 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -1,533 +1,423 @@ /* -* AMRIT – Accessible Medical Records via Integrated Technology -* 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/. -*/ - + * AMRIT – Accessible Medical Records via Integrated Technology + * 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.admin.service.health; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PreDestroy; +import javax.sql.DataSource; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.Statement; import java.time.Instant; import java.util.LinkedHashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.ExecutionException; -import java.util.function.Supplier; -import jakarta.annotation.PreDestroy; -import javax.sql.DataSource; -import com.zaxxer.hikari.HikariDataSource; -import com.zaxxer.hikari.HikariPoolMXBean; -import java.lang.management.ManagementFactory; -import javax.management.MBeanServer; -import javax.management.ObjectName; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Service; - -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; @Service public class HealthService { private static final Logger logger = LoggerFactory.getLogger(HealthService.class); - private static final String STATUS_KEY = "status"; - private static final String STATUS_UP = "UP"; - private static final String STATUS_DOWN = "DOWN"; - private static final String STATUS_DEGRADED = "DEGRADED"; - private static final String SEVERITY_KEY = "severity"; - private static final String SEVERITY_OK = "OK"; - private static final String SEVERITY_WARNING = "WARNING"; + // Event log constants + private static final String LOG_EVENT_STUCK_PROCESS = "MYSQL_STUCK_PROCESS"; + private static final String LOG_EVENT_LOCK_WAIT = "MYSQL_LOCK_WAIT"; + private static final String LOG_EVENT_DEADLOCK = "MYSQL_DEADLOCK"; + private static final String LOG_EVENT_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; + private static final String LOG_EVENT_CONN_USAGE = "MYSQL_CONNECTION_USAGE"; + private static final String LOG_EVENT_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; + + // Response field constants + private static final String FIELD_STATUS = "status"; + private static final String FIELD_SEVERITY = "severity"; + private static final String FIELD_MYSQL = "mysql"; + private static final String FIELD_REDIS = "redis"; + private static final String FIELD_CHECKED_AT = "checkedAt"; + + // Severity constants private static final String SEVERITY_CRITICAL = "CRITICAL"; - private static final String ERROR_KEY = "error"; - private static final String MESSAGE_KEY = "message"; - private static final String RESPONSE_TIME_KEY = "responseTimeMs"; - private static final long MYSQL_TIMEOUT_SECONDS = 3; - private static final long REDIS_TIMEOUT_SECONDS = 3; - - private static final long ADVANCED_CHECKS_THROTTLE_SECONDS = 30; - private static final long RESPONSE_TIME_THRESHOLD_MS = 2000; - - private static final String DIAGNOSTIC_LOCK_WAIT = "MYSQL_LOCK_WAIT"; - private static final String DIAGNOSTIC_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; - private static final String DIAGNOSTIC_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; - private static final String DIAGNOSTIC_LOG_TEMPLATE = "Diagnostic: {}"; - + private static final String SEVERITY_WARNING = "WARNING"; + private static final String SEVERITY_OK = "OK"; + private static final String SEVERITY_INFO = "INFO"; + + // Database query constants + private static final String STATUS_VALUE = "Value"; + private static final String STATUS_UP = "UP"; + private static final String STATUS_DOWN = "DOWN"; + private static final String STATUS_DEGRADED = "DEGRADED"; + private static final String STATUS_NOT_CONFIGURED = "NOT_CONFIGURED"; + + // Thresholds + private static final long RESPONSE_TIME_SLOW_MS = 2000; // > 2s → SLOW + private static final int STUCK_PROCESS_THRESHOLD = 5; // > 5 stuck → WARNING + private static final int STUCK_PROCESS_SECONDS = 30; // process age in seconds + private static final int LONG_TXN_WARNING_THRESHOLD = 1; // ≥1 long txn → WARNING + private static final int LONG_TXN_CRITICAL_THRESHOLD = 5; // ≥5 long txns → CRITICAL + private static final int LONG_TXN_SECONDS = 60; // transaction age threshold + private static final int CONNECTION_USAGE_WARNING = 80; // > 80% → WARNING + private static final int CONNECTION_USAGE_CRITICAL= 95; // > 95% → CRITICAL + private static final long DIAGNOSTIC_INTERVAL_SEC = 30; // background run interval + private static final long DIAGNOSTIC_GUARD_SEC = 25; // safety dedup guard private final DataSource dataSource; - private final RedisTemplate redisTemplate; - private final ExecutorService executorService; - - private volatile long lastAdvancedCheckTime = 0; - private volatile AdvancedCheckResult cachedAdvancedCheckResult = null; - private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); - private final AtomicBoolean advancedCheckInProgress = new AtomicBoolean(false); - - @Value("${health.advanced.checks.enabled:true}") - private boolean advancedHealthChecksEnabled; - - public HealthService(DataSource dataSource, - @Autowired(required = false) RedisTemplate redisTemplate) { - this.dataSource = dataSource; - this.redisTemplate = redisTemplate; - this.executorService = Executors.newFixedThreadPool(2, new java.util.concurrent.ThreadFactory() { - private final AtomicInteger threadCounter = new AtomicInteger(0); - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r, "health-check-" + threadCounter.incrementAndGet()); - t.setDaemon(true); - return t; - } + private final RedisConnectionFactory redisConnectionFactory; + + private final ScheduledExecutorService diagnosticScheduler = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "mysql-diagnostic-thread"); + t.setDaemon(true); + return t; }); + + private final AtomicLong lastDiagnosticRunAt = new AtomicLong(0); + private final AtomicReference cachedDbSeverity = + new AtomicReference<>(SEVERITY_OK); + private final AtomicLong previousDeadlockCount = new AtomicLong(0); + private final AtomicLong previousSlowQueryCount = new AtomicLong(0); + public HealthService(ObjectProvider dataSourceProvider, + ObjectProvider redisProvider) { + this.dataSource = dataSourceProvider.getIfAvailable(); + this.redisConnectionFactory = redisProvider.getIfAvailable(); + + // Start background diagnostics only if DB is configured. + // Initial delay = 0 so the first run happens at startup. + if (this.dataSource != null) { + diagnosticScheduler.scheduleAtFixedRate( + this::runAdvancedMySQLDiagnostics, + 0, + DIAGNOSTIC_INTERVAL_SEC, + TimeUnit.SECONDS + ); + } } @PreDestroy - public void shutdown() { - if (executorService != null && !executorService.isShutdown()) { - try { - executorService.shutdown(); - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - executorService.shutdownNow(); - logger.warn("ExecutorService did not terminate gracefully"); - } - } catch (InterruptedException e) { - executorService.shutdownNow(); - Thread.currentThread().interrupt(); - logger.warn("ExecutorService shutdown interrupted", e); + public void shutdownDiagnostics() { + logger.info("[HEALTH_SERVICE_SHUTDOWN] Shutting down diagnostic scheduler..."); + diagnosticScheduler.shutdown(); + try { + if (!diagnosticScheduler.awaitTermination(5, TimeUnit.SECONDS)) { + logger.warn("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler did not terminate gracefully"); + diagnosticScheduler.shutdownNow(); } + logger.info("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler shut down successfully"); + } catch (InterruptedException e) { + logger.error("[HEALTH_SERVICE_SHUTDOWN] Interrupted while shutting down scheduler", e); + diagnosticScheduler.shutdownNow(); + Thread.currentThread().interrupt(); } } - + + // PUBLIC — Called by the /health controller public Map checkHealth() { Map response = new LinkedHashMap<>(); - response.put("timestamp", Instant.now().toString()); - - Map mysqlStatus = new ConcurrentHashMap<>(); - Map redisStatus = new ConcurrentHashMap<>(); - - if (!executorService.isShutdown()) { - performHealthChecks(mysqlStatus, redisStatus); - } - - ensurePopulated(mysqlStatus, "MySQL"); - ensurePopulated(redisStatus, "Redis"); + + Map mysqlResult = checkDatabaseConnectivity(); + Map redisResult = checkRedisConnectivity(); + + String mysqlStatus = (String) mysqlResult.get(FIELD_STATUS); + String redisStatus = (String) redisResult.get(FIELD_STATUS); + + boolean overallUp = !STATUS_DOWN.equals(mysqlStatus) && !STATUS_DOWN.equals(redisStatus); + + response.put(FIELD_STATUS, overallUp ? STATUS_UP : STATUS_DOWN); + response.put(FIELD_CHECKED_AT, Instant.now().toString()); - Map> components = new LinkedHashMap<>(); - components.put("mysql", mysqlStatus); - components.put("redis", redisStatus); + // Expose only status and severity, keep diagnostics internal + Map mysqlSummary = new LinkedHashMap<>(); + mysqlSummary.put(FIELD_STATUS, mysqlResult.get(FIELD_STATUS)); + mysqlSummary.put(FIELD_SEVERITY, mysqlResult.get(FIELD_SEVERITY)); - response.put("components", components); - response.put(STATUS_KEY, computeOverallStatus(components)); + Map redisSummary = new LinkedHashMap<>(); + redisSummary.put(FIELD_STATUS, redisResult.get(FIELD_STATUS)); + redisSummary.put(FIELD_SEVERITY, redisResult.get(FIELD_SEVERITY)); + response.put(FIELD_MYSQL, mysqlSummary); + response.put(FIELD_REDIS, redisSummary); + return response; } - - private void performHealthChecks(Map mysqlStatus, Map redisStatus) { - Future mysqlFuture = null; - Future redisFuture = null; - try { - mysqlFuture = executorService.submit( - () -> performHealthCheck("MySQL", mysqlStatus, this::checkMySQLHealthSync)); - redisFuture = executorService.submit( - () -> performHealthCheck("Redis", redisStatus, this::checkRedisHealthSync)); - - awaitHealthChecks(mysqlFuture, redisFuture); - } catch (TimeoutException e) { - logger.warn("Health check aggregate timeout after {} seconds", getMaxTimeout()); - cancelFutures(mysqlFuture, redisFuture); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.warn("Health check was interrupted"); - cancelFutures(mysqlFuture, redisFuture); - } catch (Exception e) { - logger.warn("Health check execution error: {}", e.getMessage()); - cancelFutures(mysqlFuture, redisFuture); + // Runs only SELECT 1 with a hard 3-second timeout on query execution. + // NOTE: getConnection() is NOT bounded by this timeout — it respects the pool's + // connectionTimeout (default 30s in HikariCP). For true 3-second /health guarantees, + // configure the DataSource connectionTimeout ≤ 3 seconds or wrap in an ExecutorService timeout. + private Map checkDatabaseConnectivity() { + Map result = new LinkedHashMap<>(); + + if (dataSource == null) { + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); + return result; } - } - private void awaitHealthChecks(Future mysqlFuture, Future redisFuture) throws TimeoutException, InterruptedException, ExecutionException { - long maxTimeout = getMaxTimeout(); - long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeout); - - mysqlFuture.get(maxTimeout, TimeUnit.SECONDS); - long remainingNs = deadlineNs - System.nanoTime(); - - if (remainingNs > 0) { - redisFuture.get(remainingNs, TimeUnit.NANOSECONDS); - } else { - redisFuture.cancel(true); + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + + stmt.setQueryTimeout(3); // Bounds only the SELECT 1 execution + stmt.execute("SELECT 1"); + + // If SELECT 1 succeeds, use cached severity from background diagnostics + String severity = cachedDbSeverity.get(); + result.put(FIELD_STATUS, resolveDatabaseStatus(severity)); + result.put(FIELD_SEVERITY, severity); + + } catch (Exception e) { + // Log connection failure as a structured event + logger.error( + "[MYSQL_CONNECT_FAILED] MySQL connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); } - } - private long getMaxTimeout() { - return Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS) + 1; + return result; } - private void cancelFutures(Future mysqlFuture, Future redisFuture) { - if (mysqlFuture != null) mysqlFuture.cancel(true); - if (redisFuture != null) redisFuture.cancel(true); - } + private Map checkRedisConnectivity() { + Map result = new LinkedHashMap<>(); - private void ensurePopulated(Map status, String componentName) { + if (redisConnectionFactory == null) { + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); + return result; + } - status.putIfAbsent(STATUS_KEY, STATUS_DOWN); - status.putIfAbsent(SEVERITY_KEY, SEVERITY_CRITICAL); - status.putIfAbsent(ERROR_KEY, componentName + " health check did not complete in time"); - } + try (RedisConnection conn = redisConnectionFactory.getConnection()) { + conn.ping(); + result.put(FIELD_STATUS, STATUS_UP); + result.put(FIELD_SEVERITY, SEVERITY_OK); - private HealthCheckResult checkMySQLHealthSync() { - try (Connection connection = dataSource.getConnection(); - PreparedStatement stmt = connection.prepareStatement("SELECT 1 as health_check")) { - - stmt.setQueryTimeout((int) MYSQL_TIMEOUT_SECONDS); - - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - // Connection is auto-closed by try-with-resources here - // Advanced checks will open a separate connection if needed - } - } - } catch (Exception e) { - logger.warn("MySQL health check failed: {}", e.getMessage(), e); - return new HealthCheckResult(false, "MySQL connection failed", false); + logger.error( + "[REDIS_CONNECT_FAILED] Redis connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); } - - boolean isDegraded = performAdvancedMySQLChecksWithThrottle(); - return new HealthCheckResult(true, null, isDegraded); + return result; } - private HealthCheckResult checkRedisHealthSync() { - if (redisTemplate == null) { - return new HealthCheckResult(true, "Redis not configured — skipped", false); + private void runAdvancedMySQLDiagnostics() { + // Dedup guard: skip if last run was within the past 25 seconds + long now = System.currentTimeMillis(); + if (now - lastDiagnosticRunAt.get() < TimeUnit.SECONDS.toMillis(DIAGNOSTIC_GUARD_SEC)) { + return; } - - try { - String pong = redisTemplate.execute((org.springframework.data.redis.core.RedisCallback) (connection) -> connection.ping()); - - if ("PONG".equals(pong)) { - return new HealthCheckResult(true, null, false); - } - - return new HealthCheckResult(false, "Redis PING failed", false); - + lastDiagnosticRunAt.set(now); + + String worstSeverity = SEVERITY_OK; + + try (Connection conn = dataSource.getConnection()) { + worstSeverity = escalate(worstSeverity, performStuckProcessCheck(conn)); + worstSeverity = escalate(worstSeverity, performLongTransactionCheck(conn)); + worstSeverity = escalate(worstSeverity, performDeadlockCheck(conn)); + worstSeverity = escalate(worstSeverity, performSlowQueryCheck(conn)); + worstSeverity = escalate(worstSeverity, performConnectionUsageCheck(conn)); + } catch (Exception e) { - logger.warn("Redis health check failed: {}", e.getMessage(), e); - return new HealthCheckResult(false, "Redis connection failed", false); + logger.error( + "[MYSQL_DIAGNOSTIC_ERROR] Could not open connection for diagnostics | error=\"{}\"", + e.getMessage() + ); + worstSeverity = SEVERITY_CRITICAL; } + + cachedDbSeverity.set(worstSeverity); + logger.debug( + "[MYSQL_DIAGNOSTIC_COMPLETE] Background diagnostic cycle complete | severity={}", + worstSeverity + ); } - private Map performHealthCheck(String componentName, - Map status, - Supplier checker) { - long startTime = System.currentTimeMillis(); - - try { - HealthCheckResult result = checker.get(); - long responseTime = System.currentTimeMillis() - startTime; - - String componentStatus; - if (!result.isHealthy) { - componentStatus = STATUS_DOWN; - } else if (result.isDegraded) { - componentStatus = STATUS_DEGRADED; - } else { - componentStatus = STATUS_UP; - } - status.put(STATUS_KEY, componentStatus); - - status.put(RESPONSE_TIME_KEY, responseTime); - - String severity = determineSeverity(result.isHealthy, responseTime, result.isDegraded); - status.put(SEVERITY_KEY, severity); + private String performStuckProcessCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.PROCESSLIST " + + "WHERE TIME > " + STUCK_PROCESS_SECONDS + " AND COMMAND != 'Sleep'")) { - if (result.error != null) { - String fieldKey = result.isHealthy ? MESSAGE_KEY : ERROR_KEY; - status.put(fieldKey, result.error); + if (rs.next()) { + int stuckCount = rs.getInt("cnt"); + if (stuckCount > 0) { + if (stuckCount > STUCK_PROCESS_THRESHOLD) { + logger.warn( + "[{}] Stuck MySQL processes detected above threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); + return SEVERITY_WARNING; + } else { + logger.info( + "[{}] Stuck MySQL processes below threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); + } + } } - - return status; - } catch (Exception e) { - long responseTime = System.currentTimeMillis() - startTime; - logger.error("{} health check failed with exception: {}", componentName, e.getMessage(), e); - - status.put(STATUS_KEY, STATUS_DOWN); - status.put(RESPONSE_TIME_KEY, responseTime); - status.put(SEVERITY_KEY, SEVERITY_CRITICAL); - status.put(ERROR_KEY, "Health check failed with an unexpected error"); - - return status; - } - } - - private String determineSeverity(boolean isHealthy, long responseTimeMs, boolean isDegraded) { - if (!isHealthy) { - return SEVERITY_CRITICAL; - } - - if (isDegraded) { - return SEVERITY_WARNING; + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Stuck process check failed | error=\"{}\"", + e.getMessage()); } - - if (responseTimeMs > RESPONSE_TIME_THRESHOLD_MS) { - return SEVERITY_WARNING; - } - return SEVERITY_OK; } - private String computeOverallStatus(Map> components) { - boolean hasCritical = false; - boolean hasDegraded = false; - - for (Map componentStatus : components.values()) { - String status = (String) componentStatus.get(STATUS_KEY); - String severity = (String) componentStatus.get(SEVERITY_KEY); - - if (STATUS_DOWN.equals(status) || SEVERITY_CRITICAL.equals(severity)) { - hasCritical = true; - } - - if (STATUS_DEGRADED.equals(status)) { - hasDegraded = true; - } + private String performLongTransactionCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.INNODB_TRX " + + "WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > " + LONG_TXN_SECONDS)) { - if (SEVERITY_WARNING.equals(severity)) { - hasDegraded = true; - } - } - - if (hasCritical) { - return STATUS_DOWN; - } - - if (hasDegraded) { - return STATUS_DEGRADED; - } - - return STATUS_UP; - } - - private boolean performAdvancedMySQLChecksWithThrottle() { - if (!advancedHealthChecksEnabled) { - return false; - } - - long currentTime = System.currentTimeMillis(); - - advancedCheckLock.readLock().lock(); - try { - if (cachedAdvancedCheckResult != null && - (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { - return cachedAdvancedCheckResult.isDegraded; - } - } finally { - advancedCheckLock.readLock().unlock(); - } - - // Only one thread may submit; others fall back to the (stale) cache - if (!advancedCheckInProgress.compareAndSet(false, true)) { - advancedCheckLock.readLock().lock(); - try { - return cachedAdvancedCheckResult != null && cachedAdvancedCheckResult.isDegraded; - } finally { - advancedCheckLock.readLock().unlock(); - } - } - - try { - // Perform DB I/O outside the write lock to avoid lock contention - AdvancedCheckResult result; - try (Connection connection = dataSource.getConnection()) { - result = performAdvancedMySQLChecks(connection); - } catch (Exception e) { - if (e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); + if (rs.next()) { + int lockCount = rs.getInt("cnt"); + if (lockCount >= LONG_TXN_WARNING_THRESHOLD) { + logger.warn( + "[{}] InnoDB long-running transaction(s) detected | count={} | thresholdSeconds={}", + LOG_EVENT_LOCK_WAIT, lockCount, LONG_TXN_SECONDS + ); + // Graduated escalation: WARNING for 1-4, CRITICAL for 5+ + return lockCount >= LONG_TXN_CRITICAL_THRESHOLD + ? SEVERITY_CRITICAL : SEVERITY_WARNING; } - logger.debug("Failed to get connection for advanced checks: {}", e.getMessage()); - result = new AdvancedCheckResult(false); - } - - // Re-acquire write lock only to update the cache atomically - advancedCheckLock.writeLock().lock(); - try { - lastAdvancedCheckTime = currentTime; - cachedAdvancedCheckResult = result; - return result.isDegraded; - } finally { - advancedCheckLock.writeLock().unlock(); } - } finally { - advancedCheckInProgress.set(false); + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Long transaction check failed | error=\"{}\"", + e.getMessage()); } + return SEVERITY_OK; } - private AdvancedCheckResult performAdvancedMySQLChecks(Connection connection) { - try { - boolean hasIssues = false; - - if (hasLockWaits(connection)) { - logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_LOCK_WAIT); - hasIssues = true; - } + private String performDeadlockCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Innodb_deadlocks'")) { - - if (hasSlowQueries(connection)) { - logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_SLOW_QUERIES); - hasIssues = true; - } - - if (hasConnectionPoolExhaustion()) { - logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_POOL_EXHAUSTED); - hasIssues = true; + if (rs.next()) { + long currentDeadlocks = rs.getLong(STATUS_VALUE); + long previousDeadlocks = previousDeadlockCount.getAndSet(currentDeadlocks); + + if (currentDeadlocks > previousDeadlocks) { + long deltaDeadlocks = currentDeadlocks - previousDeadlocks; + logger.warn( + "[{}] InnoDB deadlocks detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_DEADLOCK, deltaDeadlocks, currentDeadlocks + ); + return SEVERITY_WARNING; + } } - - return new AdvancedCheckResult(hasIssues); } catch (Exception e) { - logger.debug("Advanced MySQL checks encountered exception, marking degraded"); - return new AdvancedCheckResult(true); + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Deadlock check failed | error=\"{}\"", + e.getMessage()); } + return SEVERITY_OK; } - private boolean hasLockWaits(Connection connection) { - try (PreparedStatement stmt = connection.prepareStatement( - "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + - "WHERE (state = 'Waiting for table metadata lock' " + - " OR state = 'Waiting for row lock' " + - " OR state = 'Waiting for lock') " + - "AND user = SUBSTRING_INDEX(USER(), '@', 1)")) { - stmt.setQueryTimeout(2); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - int lockCount = rs.getInt(1); - return lockCount > 0; + private String performSlowQueryCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Slow_queries'")) { + + if (rs.next()) { + long slowQueries = rs.getLong(STATUS_VALUE); + long previousSlow = previousSlowQueryCount.getAndSet(slowQueries); + + // Only warn if slow queries have *increased* since last run + if (slowQueries > previousSlow) { + long delta = slowQueries - previousSlow; + logger.warn( + "[{}] New slow queries detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_SLOW_QUERIES, delta, slowQueries + ); + return SEVERITY_WARNING; } } } catch (Exception e) { - logger.debug("Could not check for lock waits"); + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Slow query check failed | error=\"{}\"", + e.getMessage()); } - return false; + return SEVERITY_OK; } - private boolean hasSlowQueries(Connection connection) { - try (PreparedStatement stmt = connection.prepareStatement( - "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + - "WHERE command != 'Sleep' AND time > ? AND user = SUBSTRING_INDEX(USER(), '@', 1)")) { - stmt.setQueryTimeout(2); - stmt.setInt(1, 10); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - int slowQueryCount = rs.getInt(1); - return slowQueryCount > 3; - } + private String performConnectionUsageCheck(Connection conn) { + try (Statement stmt = conn.createStatement()) { + int threadsConnected = 0; + int maxConnections = 0; + + try (ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Threads_connected'")) { + if (rs.next()) threadsConnected = rs.getInt(STATUS_VALUE); } - } catch (Exception e) { - logger.debug("Could not check for slow queries"); - } - return false; - } - private boolean hasConnectionPoolExhaustion() { - if (dataSource instanceof HikariDataSource hikariDataSource) { - try { - HikariPoolMXBean poolMXBean = hikariDataSource.getHikariPoolMXBean(); - - if (poolMXBean != null) { - int activeConnections = poolMXBean.getActiveConnections(); - int maxPoolSize = hikariDataSource.getMaximumPoolSize(); - - int threshold = (int) (maxPoolSize * 0.8); - return activeConnections > threshold; - } - } catch (Exception e) { - logger.debug("Could not retrieve HikariCP pool metrics"); + try (ResultSet rs = stmt.executeQuery("SHOW VARIABLES LIKE 'max_connections'")) { + if (rs.next()) maxConnections = rs.getInt(STATUS_VALUE); } - } - - return checkPoolMetricsViaJMX(); - } - private boolean checkPoolMetricsViaJMX() { - try { - MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); - ObjectName objectName = new ObjectName("com.zaxxer.hikari:type=Pool (*)"); - var mBeans = mBeanServer.queryMBeans(objectName, null); - - for (var mBean : mBeans) { - if (evaluatePoolMetrics(mBeanServer, mBean.getObjectName())) { - return true; + if (maxConnections > 0) { + int usagePct = (int) ((threadsConnected * 100.0) / maxConnections); + + if (usagePct >= CONNECTION_USAGE_CRITICAL) { + logger.error( + "[{}] MySQL connection pool near exhaustion | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_POOL_EXHAUSTED, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_CRITICAL; + + } else if (usagePct > CONNECTION_USAGE_WARNING) { + logger.warn( + "[{}] MySQL connection usage is high | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_CONN_USAGE, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_WARNING; } } } catch (Exception e) { - logger.debug("Could not access HikariCP pool metrics via JMX"); + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Connection usage check failed | error=\"{}\"", + e.getMessage()); } - - logger.debug("Pool exhaustion check disabled: HikariCP metrics unavailable"); - return false; + return SEVERITY_OK; } - - private boolean evaluatePoolMetrics(MBeanServer mBeanServer, ObjectName objectName) { - try { - Integer activeConnections = (Integer) mBeanServer.getAttribute(objectName, "ActiveConnections"); - Integer maximumPoolSize = (Integer) mBeanServer.getAttribute(objectName, "MaximumPoolSize"); - - if (activeConnections != null && maximumPoolSize != null) { - int threshold = (int) (maximumPoolSize * 0.8); - return activeConnections > threshold; - } - } catch (Exception e) { - // Continue to next MBean - } - return false; + private String resolveDatabaseStatus(String severity) { + return switch (severity) { + case SEVERITY_CRITICAL -> STATUS_DOWN; + case SEVERITY_WARNING -> STATUS_DEGRADED; + default -> STATUS_UP; + }; } - - private static class AdvancedCheckResult { - final boolean isDegraded; - - AdvancedCheckResult(boolean isDegraded) { - this.isDegraded = isDegraded; - } + private String escalate(String current, String candidate) { + return severityRank(candidate) > severityRank(current) ? candidate : current; } - private static class HealthCheckResult { - final boolean isHealthy; - final String error; - final boolean isDegraded; - - HealthCheckResult(boolean isHealthy, String error, boolean isDegraded) { - this.isHealthy = isHealthy; - this.error = error; - this.isDegraded = isDegraded; - } + private int severityRank(String severity) { + return switch (severity) { + case SEVERITY_CRITICAL -> 2; + case SEVERITY_WARNING -> 1; + default -> 0; + }; } -} +} \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/service/version/VersionService.java b/src/main/java/com/iemr/admin/service/version/VersionService.java deleted file mode 100644 index f46ce4d..0000000 --- a/src/main/java/com/iemr/admin/service/version/VersionService.java +++ /dev/null @@ -1,170 +0,0 @@ -/* -* AMRIT – Accessible Medical Records via Integrated Technology -* 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.admin.service.version; - -import java.io.IOException; -import java.io.InputStream; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Properties; -import java.util.TimeZone; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import com.fasterxml.jackson.databind.ObjectMapper; - -@Service -public class VersionService { - - private static final Logger logger = LoggerFactory.getLogger(VersionService.class); - - @Value("${app.version:unknown}") - private String appVersion; - - @Value("${maven.properties.path:META-INF/maven/com.iemr.admin/admin-api/pom.properties}") - private String mavenPropertiesPath; - - private final ObjectMapper objectMapper = new ObjectMapper(); - - /** - * Get version information as a Map for direct JSON serialization - * @return Map containing version information - */ - public Map getVersionInfo() { - return buildVersionInfo(); - } - - /** - * Get version information as JSON string (deprecated - use getVersionInfo() instead) - * @return JSON string containing version information - */ - @Deprecated - public String getVersionInformation() { - try { - Map versionInfo = buildVersionInfo(); - return objectMapper.writeValueAsString(versionInfo); - } catch (Exception e) { - logger.error("Error building version information", e); - return createErrorResponse(); - } - } - - private Map buildVersionInfo() { - Map versionInfo = new LinkedHashMap<>(); - - // Add Git information - addGitInformation(versionInfo); - - // Add build information - addBuildInformation(versionInfo); - - // Add current time - versionInfo.put("current.time", getCurrentIstTimeFormatted()); - - return versionInfo; - } - - private void addGitInformation(Map versionInfo) { - Properties gitProps = loadPropertiesFile("git.properties"); - if (gitProps != null) { - String commitId = gitProps.getProperty("git.commit.id", - gitProps.getProperty("git.commit.id.abbrev", "unknown")); - versionInfo.put("git.commit.id", commitId); - - String buildTime = gitProps.getProperty("git.build.time", - gitProps.getProperty("git.commit.time", - gitProps.getProperty("git.commit.timestamp", "unknown"))); - versionInfo.put("git.build.time", buildTime); - } else { - logger.warn("git.properties file not found. Git information will be unavailable."); - versionInfo.put("git.commit.id", "information unavailable"); - versionInfo.put("git.build.time", "information unavailable"); - } - } - - private void addBuildInformation(Map versionInfo) { - Properties buildProps = loadPropertiesFile("META-INF/build-info.properties"); - if (buildProps != null) { - String version = buildProps.getProperty("build.version", - buildProps.getProperty("build.version.number", - buildProps.getProperty("version", appVersion))); - versionInfo.put("build.version", version); - - String time = buildProps.getProperty("build.time", - buildProps.getProperty("build.timestamp", - buildProps.getProperty("timestamp", getCurrentIstTimeFormatted()))); - versionInfo.put("build.time", time); - } else { - logger.info("build-info.properties not found, trying Maven properties"); - Properties mavenProps = loadPropertiesFile(mavenPropertiesPath); - if (mavenProps != null) { - String version = mavenProps.getProperty("version", appVersion); - versionInfo.put("build.version", version); - versionInfo.put("build.time", getCurrentIstTimeFormatted()); - } else { - logger.warn("Neither build-info.properties nor Maven properties found."); - versionInfo.put("build.version", appVersion); - versionInfo.put("build.time", getCurrentIstTimeFormatted()); - } - } - } - - private String getCurrentIstTimeFormatted() { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - sdf.setTimeZone(TimeZone.getTimeZone("Asia/Kolkata")); - return sdf.format(new Date()); - } - - private Properties loadPropertiesFile(String resourceName) { - ClassLoader classLoader = getClass().getClassLoader(); - try (InputStream inputStream = classLoader.getResourceAsStream(resourceName)) { - if (inputStream != null) { - Properties props = new Properties(); - props.load(inputStream); - return props; - } - } catch (IOException e) { - logger.warn("Could not load properties file: " + resourceName, e); - } - return null; - } - - private String createErrorResponse() { - try { - Map errorInfo = new LinkedHashMap<>(); - errorInfo.put("git.commit.id", "error retrieving information"); - errorInfo.put("git.build.time", "error retrieving information"); - errorInfo.put("build.version", appVersion); - errorInfo.put("build.time", getCurrentIstTimeFormatted()); - errorInfo.put("current.time", getCurrentIstTimeFormatted()); - return objectMapper.writeValueAsString(errorInfo); - } catch (Exception e) { - logger.error("Error creating error response", e); - return "{\"error\": \"Unable to retrieve version information\"}"; - } - } -} From 235e63eb816637a16e8834772d7b2dc676547c76 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:11:59 +0530 Subject: [PATCH 3/8] Vb/release 3.8.2 merge (#139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bulk registration * Bulk registration * Bulk registration * add userName and password in Bulk registration * add userName and password in Bulk registration * remove unwanted line * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * Httpheader content disposition changed * Coderabbitai comments adrressed * Httpheader content disposition changed (#100) * Httpheader content disposition changed * Coderabbitai comments adrressed * Compile error resolved * fix code * Main branch changes missed (#102) * Feature/signaturerelease (#103) * Main branch changes missed * Signature file changed * Feature/signaturerelease (#104) * Main branch changes missed * Signature file changed * Created new endpoint for Active and DeActive Employee Signature * coderabbit comments addressed * Update pom.xml * Cherry-pick the commits related to 3.5.0 form 3.6.0 (#106) * Compile error resolved * fix: cherry pic the #101 pr * fix: cherry pic the #101 pr * Main branch changes missed * fix: cherry pic the #103 pr * fix: cherry pic the #104 pr * fix: cherry pic the #104 pr * fix: cherry pick the #104 pr * fix code rabbit comments --------- Co-authored-by: Ravi Shanigarapu * fix:casesheet signature * fix:pom file change * API changes in Signature enhancement for Casesheet (#107) * fix:casesheet signature * fix:pom file change * fix: pom version * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix: amm-1927 send headers only if the request is from the allowed origin * fix: amm-1927 coderabbit fixes * Update regex handling for localhost URLs * Enhance regex pattern for URL matching * fix code * fix code * fix vulnerabilitie code * fix security hotspots * fixed conflicts * Add facility hierarchy creation with village and parent-child mapping (#121) * fix:changed the pom xml * fix: added facilty type master change * feat: created facility creation * fix: rabiit review fix * fix: rabiit review fix * fix: rabiit review fix * fix: pom version * fix: facility hierarchy and facility type management (#125) * fix:changed the pom xml * fix: added facilty type master change * feat: created facility creation * feat:added work location * feat:added work location * fix: rabiit review fix * fix: rabiit review fix * fix: rabiit review fix * fix: ui chnges * fix: pom version * fix: corrections * fix: facilty hierachy * fix: facility heirachy * fix: remove logs folder from repository Co-Authored-By: Claude Opus 4.6 (1M context) * fix: add logs/ to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) * Facility hierarchy inventory mapping and store field management (#128) * fix:changed the pom xml * fix: added facilty type master change * feat: created facility creation * feat:added work location * feat:added work location * fix: rabiit review fix * fix: rabiit review fix * fix: rabiit review fix * fix: ui chnges * fix: pom version * fix: corrections * fix: facilty hierachy * fix: facility heirachy * fix: item facility mapping and store updates Co-Authored-By: Claude Opus 4.6 (1M context) * fix: fixed inventory flow --------- Co-authored-by: Claude Opus 4.6 (1M context) * merge 3.6.2 to release 3.8.1 (#132) * Cherry-pick health and version API enhancements to release-3.6.1 (#124) * feat(health,version): add health and version endponts * fix(health): add constant and remove duplicates * fix(health): avoid permanent DEGRADED from historical deadlocks * fix(health): Removed the unnecessary boolean literal * fix(health): Fixed the broken lock-wait detection * fix(health): avoid blocking DB I/O under write lock and restore interrupt flag * fix(health): add cancelFutures in healthservice * fix(health): close basic DB connection before advanced checks and remove shared-map race * feat: expose account lock state in SearchEmployee4 (#129) Co-authored-by: Varun Deep Saini * Fix the reset password issue (#131) * fix: reset password * fix: build issue * fix: update the url * fix: update config properties * fix: initialize constructor --------- Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: Varun Deep Saini Co-authored-by: Varun Deep Saini Co-authored-by: Vanitha S <116701245+vanitha1822@users.noreply.github.com> * fix: avoid ONLY_FULL_GROUP_BY violation in service lookup queries GROUP BY sm.serviceName selected non-aggregated, non-functionally-dependent columns (serviceID, isNational, statusID), which MySQL rejects under ONLY_FULL_GROUP_BY. Use SELECT DISTINCT instead since no aggregation is actually needed. * fix: bulk employee registration sets username from contact number instead of username field mUser.setUserName(employee.getContactNo()) was using the uploaded contact number as the login username, ignoring the actual UserName column in the bulk upload sheet. Co-Authored-By: Claude Sonnet 4.6 * Add Nikshay location master for Stop TB (TU/Facility) and complete duplicate-mapping fix - New isolated tables/entities: NikshayTU, NikshayFacility, NikshayVillageFacilityMapping, with read-only cascading lookup endpoints (NikshayLocationController) — no existing master tables touched. - M_UserServiceRoleMapping2 gains NikshayTUID/NikshayFacilityID (nullable, additive), persisted on both create and update save paths. - Complete the pre-existing softDeleteOldMappings fix by adding the missing service interface/impl methods the controller already called. Co-Authored-By: Claude Sonnet 5 * feat(nikshay): replace AMRIT-village-matching with independent Nikshay hierarchy Nikshay's TU/Facility/Village hierarchy was previously matched against AMRIT's existing state/district/block/village masters by name. That approach left ~44% of villages unmatched in practice — partly from incomplete migration runs, partly from real staleness in AMRIT's own district/block data (e.g. Andhra Pradesh's 2022 district reorganization never propagated to m_districtblock/m_DistrictBranchMapping). Replace it with a fully self-contained Nikshay hierarchy (NikshayState, NikshayDistrict, NikshayTU, NikshayFacility, NikshayVillage) sourced directly from Nikshay's own imported data, with no AMRIT matching involved. Verified: 100% village coverage (325,426/325,426) vs the previous approach's ~56%. NikshayLocationController's endpoints keep the same paths and query params, now backed by the new tables. * fix(nikshay): store NikshayTUID/NikshayFacilityID as comma-joined String Admin-UI now saves one row per user-role with a comma-joined list of TU/Facility IDs instead of one row per TU x Facility combination, so the DB columns became TEXT. Update the entity and the request-body DTO (Previleges1097_3) to match — both were still Integer, which would have failed to deserialize a value like "12,45,78". Co-Authored-By: Claude Sonnet 5 * feat(nikshay): add endpoint to read back saved TU/Facility/District for Edit v_userservicerolemapping (and its entity, what the Admin-UI's mapped-list screen reads) never exposes DistrictID/NikshayTUID/NikshayFacilityID, only legacy AMRIT WorkingDistrictID/WorkingDistrictName - always null for Stop TB, which never populates WorkingLocationID. Edit had no way to read back what was saved, so the TU/Facility/Village pickers always loaded empty. Reads m_userservicerolemapping directly by USRMappingID (reusing the existing EmployeeMasterRepo.findByUSRMappingID), same pattern as the FLW-API worklist-scope query - additive only, the shared view and every other service line reading it stay untouched. Co-Authored-By: Claude Sonnet 5 * fix: add missing @Transactional to softDeleteOldMappings InvalidDataAccessApiUsageException: "Executing an update/delete query" - a @Modifying UPDATE query with no surrounding transaction. Pre-existing bug in shared code (updateUserRoleMapping, used by every service line), dormant until now: Stop TB's Edit form was always invalid before today's fixes (Update button permanently disabled), so nothing ever reached this code path through Stop TB specifically. Same pattern the other @Modifying query in this file already follows correctly. Co-Authored-By: Claude Sonnet 5 * fix(nikshay): stop patching District with AMRIT lookup for Stop TB getMappedRole()'s null-district patch (getDirectStateDistrictByMappingIDs) joins the raw DistrictID column against AMRIT's m_district - correct for HWC/FLW, where that column genuinely holds an AMRIT district ID. Stop TB's DistrictID holds a Nikshay district ID instead, so the same join resolves to whatever AMRIT district happens to share that numeric ID by coincidence (e.g. Nikshay's Angul, ID 292, resolving to AMRIT's Uttar Kannad, also ID 292) - showing a completely wrong, unrelated district name in the mapped-users list. Skip the District patch for Stop TB rows specifically; State/Block patching is untouched since those values are genuinely AMRIT-sourced even for Stop TB. Co-Authored-By: Claude Sonnet 5 * Add Nikshay location master for Stop TB (TU/Facility) and complete duplicate-mapping fix - New isolated tables/entities: NikshayTU, NikshayFacility, NikshayVillageFacilityMapping, with read-only cascading lookup endpoints (NikshayLocationController) — no existing master tables touched. - M_UserServiceRoleMapping2 gains NikshayTUID/NikshayFacilityID (nullable, additive), persisted on both create and update save paths. - Complete the pre-existing softDeleteOldMappings fix by adding the missing service interface/impl methods the controller already called. Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: Saurav Mishra Co-authored-by: Sushant <77480199+sushant-bizbrolly@users.noreply.github.com> Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Mithun James Co-authored-by: Ravi Shanigarapu Co-authored-by: ravishanigarapu <133210792+ravishanigarapu@users.noreply.github.com> Co-authored-by: Amoghavarsh <93114621+5Amogh@users.noreply.github.com> Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: 5Amogh Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: Varun Deep Saini Co-authored-by: Varun Deep Saini Co-authored-by: Vanitha S <116701245+vanitha1822@users.noreply.github.com> --- .../EmployeeMasterController.java | 11 + .../nikshay/NikshayLocationController.java | 193 ++++++++++++++++++ .../M_UserServiceRoleMapping2.java | 12 ++ .../admin/data/nikshay/NikshayDistrict.java | 95 +++++++++ .../admin/data/nikshay/NikshayFacility.java | 98 +++++++++ .../iemr/admin/data/nikshay/NikshayState.java | 90 ++++++++ .../iemr/admin/data/nikshay/NikshayTU.java | 94 +++++++++ .../admin/data/nikshay/NikshayVillage.java | 93 +++++++++ .../NikshayVillageFacilityMapping.java | 83 ++++++++ .../MProviderservicemappingBlockingRepo.java | 2 +- .../employeemaster/EmployeeMasterRepo.java | 6 + .../repo/nikshay/NikshayDistrictRepo.java | 38 ++++ .../repo/nikshay/NikshayFacilityRepo.java | 41 ++++ .../admin/repo/nikshay/NikshayStateRepo.java | 37 ++++ .../admin/repo/nikshay/NikshayTURepo.java | 41 ++++ .../NikshayVillageFacilityMappingRepo.java | 42 ++++ .../repo/nikshay/NikshayVillageRepo.java | 38 ++++ ...cerolemappingForRoleProviderAdminRepo.java | 5 +- .../BulkRegistrationServiceImpl.java | 2 +- .../employeemaster/EmployeeMasterInter.java | 3 + .../EmployeeMasterServiceImpl.java | 18 +- .../EmployeeSignatureServiceImpl.java | 1 + .../to/employeemaster/Previleges1097_3.java | 16 ++ .../utils/JwtUserIdValidationFilter.java | 1 - 24 files changed, 1052 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/iemr/admin/controller/nikshay/NikshayLocationController.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayDistrict.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayFacility.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayState.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayTU.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayVillage.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayVillageFacilityMapping.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayDistrictRepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayFacilityRepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayStateRepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayTURepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageFacilityMappingRepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageRepo.java diff --git a/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java b/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java index 35b6ed6..33d980c 100644 --- a/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java +++ b/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java @@ -1813,6 +1813,8 @@ public String UserRoleMappings(@RequestBody String userRoleMapping, HttpServletR resDataMap1.setVillageID(previl.getVillageID()); resDataMap1.setVillageName(previl.getVillageName()); resDataMap1.setFacilityID(previl.getFacilityID()); + resDataMap1.setNikshayTUID(previl.getNikshayTUID()); + resDataMap1.setNikshayFacilityID(previl.getNikshayFacilityID()); resList1.add(resDataMap1); } @@ -1861,6 +1863,13 @@ public String updateUserRoleMapping(@RequestBody String updateUserRoleMapping, H } } + // Soft-delete other active mappings for same user+service to prevent duplicates + // (e.g. old roleID 122 rows left over when new roleID 128 mapping was created) + if (pre.getUserID() != null && pre.getProviderServiceMapID() != null && pre.getuSRMappingID() != null) { + employeeMasterInter.softDeleteOldMappings( + pre.getUserID(), pre.getProviderServiceMapID(), pre.getuSRMappingID()); + } + usrRole.setUserID(pre.getUserID()); usrRole.setRoleID(pre.getRoleID()); usrRole.setAgentPassword(pre.getAgentPassword()); @@ -1874,6 +1883,8 @@ public String updateUserRoleMapping(@RequestBody String updateUserRoleMapping, H usrRole.setVillageID(pre.getVillageID()); usrRole.setVillageName(pre.getVillageName()); usrRole.setFacilityID(pre.getFacilityID()); + usrRole.setNikshayTUID(pre.getNikshayTUID()); + usrRole.setNikshayFacilityID(pre.getNikshayFacilityID()); if (pre.getTeleConsultation() != null) { usrRole.setTeleConsultation(pre.getTeleConsultation()); diff --git a/src/main/java/com/iemr/admin/controller/nikshay/NikshayLocationController.java b/src/main/java/com/iemr/admin/controller/nikshay/NikshayLocationController.java new file mode 100644 index 0000000..aa8d0a8 --- /dev/null +++ b/src/main/java/com/iemr/admin/controller/nikshay/NikshayLocationController.java @@ -0,0 +1,193 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.controller.nikshay; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.google.gson.Gson; +import com.iemr.admin.data.employeemaster.M_UserServiceRoleMapping2; +import com.iemr.admin.data.nikshay.NikshayDistrict; +import com.iemr.admin.data.nikshay.NikshayFacility; +import com.iemr.admin.data.nikshay.NikshayState; +import com.iemr.admin.data.nikshay.NikshayTU; +import com.iemr.admin.data.nikshay.NikshayVillage; +import com.iemr.admin.repo.employeemaster.EmployeeMasterRepo; +import com.iemr.admin.repo.nikshay.NikshayDistrictRepo; +import com.iemr.admin.repo.nikshay.NikshayFacilityRepo; +import com.iemr.admin.repo.nikshay.NikshayStateRepo; +import com.iemr.admin.repo.nikshay.NikshayTURepo; +import com.iemr.admin.repo.nikshay.NikshayVillageRepo; +import com.iemr.admin.utils.response.OutputResponse; + +import io.swagger.v3.oas.annotations.Operation; + +/** + * Read-only cascading lookups for Stop TB's Nikshay location hierarchy: + * State -> District -> TU -> Facility -> Village — all sourced from + * Nikshay's own imported data (m_nikshay_*), independent of AMRIT's own + * state/district/village masters. Earlier versions matched against AMRIT's + * existing location tables by name, but AMRIT's own district/block data has + * real staleness (e.g. post-2022 Andhra Pradesh district reorganization + * never propagated), which left a large fraction of villages unmatched. + * This hierarchy avoids that entirely. + * + * Every endpoint here only reads existing tables — nothing is inserted or + * altered from these calls. + */ +@RestController +public class NikshayLocationController { + + private final Logger logger = LoggerFactory.getLogger(NikshayLocationController.class); + + @Autowired + private NikshayStateRepo nikshayStateRepo; + + @Autowired + private EmployeeMasterRepo employeeMasterRepo; + + @Autowired + private NikshayDistrictRepo nikshayDistrictRepo; + + @Autowired + private NikshayTURepo nikshayTURepo; + + @Autowired + private NikshayFacilityRepo nikshayFacilityRepo; + + @Autowired + private NikshayVillageRepo nikshayVillageRepo; + + @Operation(summary = "Get all Nikshay states") + @GetMapping(value = "/nikshay/location/states", produces = "application/json") + public String getStates() { + OutputResponse response = new OutputResponse(); + try { + List states = nikshayStateRepo.findAllActive(); + response.setResponse(states.toString()); + } catch (Exception e) { + logger.error("Error fetching Nikshay states: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get Nikshay districts for a Nikshay state") + @GetMapping(value = "/nikshay/location/districts", produces = "application/json") + public String getDistricts(@RequestParam("stateID") Integer stateID) { + OutputResponse response = new OutputResponse(); + try { + List districts = nikshayDistrictRepo.findByStateID(stateID); + response.setResponse(districts.toString()); + } catch (Exception e) { + logger.error("Error fetching districts for stateID " + stateID + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get Nikshay TUs for a district") + @GetMapping(value = "/nikshay/location/tus", produces = "application/json") + public String getTUs(@RequestParam("districtID") Integer districtID) { + OutputResponse response = new OutputResponse(); + try { + List tus = nikshayTURepo.findByDistrictID(districtID); + response.setResponse(tus.toString()); + } catch (Exception e) { + logger.error("Error fetching Nikshay TUs for districtID " + districtID + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get Nikshay facilities for one or more TUs (comma-separated tuIDs)") + @GetMapping(value = "/nikshay/location/facilities", produces = "application/json") + public String getFacilities(@RequestParam("tuIDs") String tuIDs) { + OutputResponse response = new OutputResponse(); + try { + List ids = parseIntCsv(tuIDs); + List facilities = nikshayFacilityRepo.findByTUIDs(ids); + response.setResponse(facilities.toString()); + } catch (Exception e) { + logger.error("Error fetching Nikshay facilities for tuIDs " + tuIDs + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get Nikshay villages for one or more Nikshay facilities (comma-separated facilityIDs)") + @GetMapping(value = "/nikshay/location/villages", produces = "application/json") + public String getVillages(@RequestParam("facilityIDs") String facilityIDs) { + OutputResponse response = new OutputResponse(); + try { + List ids = parseIntCsv(facilityIDs); + List villages = nikshayVillageRepo.findByFacilityIDs(ids); + response.setResponse(villages.toString()); + } catch (Exception e) { + logger.error("Error fetching villages for facilityIDs " + facilityIDs + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get the Nikshay DistrictID/TUID/FacilityID saved on a Stop TB " + + "user-role mapping row, by USRMappingID. Reads m_userservicerolemapping " + + "directly (NOT the shared v_userservicerolemapping view, which does not " + + "expose these Stop TB-only columns), so the view and every other service " + + "line reading it are untouched.") + @GetMapping(value = "/nikshay/location/userMapping", produces = "application/json") + public String getUserMappingNikshayData(@RequestParam("usrMappingID") Integer usrMappingID) { + OutputResponse response = new OutputResponse(); + try { + M_UserServiceRoleMapping2 row = employeeMasterRepo.findByUSRMappingID(usrMappingID); + Map result = new HashMap<>(); + if (row != null) { + result.put("districtID", row.getDistrictID()); + result.put("nikshayTUID", row.getNikshayTUID()); + result.put("nikshayFacilityID", row.getNikshayFacilityID()); + } + response.setResponse(new Gson().toJson(result)); + } catch (Exception e) { + logger.error("Error fetching Nikshay data for usrMappingID " + usrMappingID + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + private List parseIntCsv(String csv) { + return Arrays.stream(csv.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/iemr/admin/data/employeemaster/M_UserServiceRoleMapping2.java b/src/main/java/com/iemr/admin/data/employeemaster/M_UserServiceRoleMapping2.java index ad6575a..e33da77 100644 --- a/src/main/java/com/iemr/admin/data/employeemaster/M_UserServiceRoleMapping2.java +++ b/src/main/java/com/iemr/admin/data/employeemaster/M_UserServiceRoleMapping2.java @@ -423,6 +423,18 @@ public class M_UserServiceRoleMapping2 { @Column(name = "isOutbound") private Boolean outbound; + // Stop TB / Nikshay location scope — populated only when serviceName = "Stop TB". + // Comma-joined list of Nikshay TU/Facility IDs (e.g. "12,45,78") — a user + // can be mapped to several TUs/Facilities in one row, same idea as + // Villageid. + @Expose + @Column(name = "NikshayTUID") + private String nikshayTUID; + + @Expose + @Column(name = "NikshayFacilityID") + private String nikshayFacilityID; + public M_UserServiceRoleMapping2() { } diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayDistrict.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayDistrict.java new file mode 100644 index 0000000..9218944 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayDistrict.java @@ -0,0 +1,95 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's own District master — imported directly from Nikshay's location + * data, independent of AMRIT's existing district master (m_District). Kept + * separate rather than matched to AMRIT because AMRIT's own district/block + * data has been found to be stale in places (e.g. post-2022 AP district + * reorganization never propagated), which made name-matching unreliable. + */ +@Data +@Entity +@Table(name = "m_nikshay_district") +public class NikshayDistrict { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayDistrictID") + private Integer nikshayDistrictID; + + @Expose + @Column(name = "NikshayCode") + private String nikshayCode; + + @Expose + @Column(name = "DistrictName") + private String districtName; + + @Expose + @Column(name = "NikshayStateID") + private Integer nikshayStateID; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayFacility.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayFacility.java new file mode 100644 index 0000000..a383572 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayFacility.java @@ -0,0 +1,98 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's PHC/HWC/CHC facility master. Verified against production data + * that AMRIT's existing m_facility has no meaningful overlap with Nikshay's + * facility list (0 matches out of 343,615) — kept as its own table rather + * than reusing m_facility. + */ +@Data +@Entity +@Table(name = "m_nikshay_facility") +public class NikshayFacility { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayFacilityID") + private Integer nikshayFacilityID; + + @Expose + @Column(name = "NikshayCode") + private String nikshayCode; + + @Expose + @Column(name = "FacilityName") + private String facilityName; + + @Expose + @Column(name = "FacilityCode") + private String facilityCode; + + @Expose + @Column(name = "NikshayTUID") + private Integer nikshayTUID; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayState.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayState.java new file mode 100644 index 0000000..66450c3 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayState.java @@ -0,0 +1,90 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's own State master — imported directly from Nikshay's location + * data, independent of AMRIT's existing state master. No name-matching + * against AMRIT is performed; this is the top of a fully self-contained + * Nikshay hierarchy (State -> District -> TU -> Facility -> Village). + */ +@Data +@Entity +@Table(name = "m_nikshay_state") +public class NikshayState { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayStateID") + private Integer nikshayStateID; + + @Expose + @Column(name = "NikshayCode") + private String nikshayCode; + + @Expose + @Column(name = "StateName") + private String stateName; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayTU.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayTU.java new file mode 100644 index 0000000..25023b0 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayTU.java @@ -0,0 +1,94 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's TU (Tuberculosis Unit) master — Nikshay's own sub-district + * administrative unit, referred to as "Block" on some Nikshay screens. + * No equivalent exists in AMRIT's own masters; linked to Nikshay's own + * District master (NikshayDistrict), not AMRIT's m_District. + */ +@Data +@Entity +@Table(name = "m_nikshay_tu") +public class NikshayTU { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayTUID") + private Integer nikshayTUID; + + @Expose + @Column(name = "NikshayCode") + private String nikshayCode; + + @Expose + @Column(name = "TUName") + private String tUName; + + @Expose + @Column(name = "NikshayDistrictID") + private Integer nikshayDistrictID; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayVillage.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayVillage.java new file mode 100644 index 0000000..b9bfd6b --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayVillage.java @@ -0,0 +1,93 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's own Village master, linked directly to its Nikshay Facility. + * Imported straight from Nikshay's location data with no matching against + * AMRIT's village master (m_DistrictBranchMapping) — a prior approach that + * matched by name against AMRIT villages left ~44% of villages unmatched, + * partly from incomplete migration runs and partly from real staleness in + * AMRIT's own district/block data. This table guarantees full coverage of + * every village Nikshay has, independent of AMRIT's location hierarchy. + */ +@Data +@Entity +@Table(name = "m_nikshay_village") +public class NikshayVillage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayVillageID") + private Integer nikshayVillageID; + + @Expose + @Column(name = "VillageName") + private String villageName; + + @Expose + @Column(name = "NikshayFacilityID") + private Integer nikshayFacilityID; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayVillageFacilityMapping.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayVillageFacilityMapping.java new file mode 100644 index 0000000..fad7b42 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayVillageFacilityMapping.java @@ -0,0 +1,83 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Links an existing AMRIT village (m_DistrictBranchMapping.DistrictBranchID) + * to the Nikshay Facility/TU it falls under. Built once from the location + * import's name-matching results — used to trace a beneficiary's existing + * village back to their Nikshay Facility/TU without asking the field worker + * to re-select it per case. + */ +@Data +@Entity +@Table(name = "m_nikshay_village_facility_mapping") +public class NikshayVillageFacilityMapping { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "MappingID") + private Integer mappingID; + + @Expose + @Column(name = "AmritVillageID") + private Integer amritVillageID; + + @Expose + @Column(name = "NikshayFacilityID") + private Integer nikshayFacilityID; + + @Expose + @Column(name = "NikshayTUID") + private Integer nikshayTUID; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/repo/blocking/MProviderservicemappingBlockingRepo.java b/src/main/java/com/iemr/admin/repo/blocking/MProviderservicemappingBlockingRepo.java index 1b7b082..e0a0ffe 100644 --- a/src/main/java/com/iemr/admin/repo/blocking/MProviderservicemappingBlockingRepo.java +++ b/src/main/java/com/iemr/admin/repo/blocking/MProviderservicemappingBlockingRepo.java @@ -85,7 +85,7 @@ void blockProviderByProviderIdAndServiceId(@Param("serviceProviderID") Integer s @Query(" SELECT distinct srm.providerServiceMapID,srm.serviceProviderID,srm.serviceID," + " sm.serviceName as serviceName," + " sm.isNational as isNational " + " FROM M_Providerservicemapping_Blocking srm " + " JOIN srm.m_ServicemasterForBlocking sm" - + " WHERE srm.serviceProviderID =:serviceProviderID AND srm.deleted=false" + " GROUP BY sm.serviceName") + + " WHERE srm.serviceProviderID =:serviceProviderID AND srm.deleted=false") ArrayList getServiceLiensUsingProvider(@Param("serviceProviderID") Integer serviceProviderID); @Transactional diff --git a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java index 401b0c1..e2bf74f 100644 --- a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java +++ b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java @@ -155,4 +155,10 @@ boolean existsByUserIDAndRoleIDAndProviderServiceMapIDAndFacilityIDAndDeletedFal // Fix 2: count active USR rows for supervisor (check if other facilities remain) long countByUserIDAndRoleIDAndDeletedFalse(Integer userID, Integer roleID); + // Soft-delete old duplicate mappings for same user+service, excluding the current row being updated + @Transactional + @Modifying + @Query("UPDATE M_UserServiceRoleMapping2 u SET u.deleted = true WHERE u.userID = :userID AND u.providerServiceMapID = :providerServiceMapID AND u.uSRMappingID != :excludeUSRMappingID AND u.deleted = false") + int softDeleteOldMappings(@Param("userID") Integer userID, @Param("providerServiceMapID") Integer providerServiceMapID, @Param("excludeUSRMappingID") Integer excludeUSRMappingID); + } diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayDistrictRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayDistrictRepo.java new file mode 100644 index 0000000..30b6ee9 --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayDistrictRepo.java @@ -0,0 +1,38 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayDistrict; + +@Repository +public interface NikshayDistrictRepo extends CrudRepository { + + @Query("select d from NikshayDistrict d where d.nikshayStateID = :stateID and d.deleted = false order by d.districtName") + List findByStateID(@Param("stateID") Integer stateID); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayFacilityRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayFacilityRepo.java new file mode 100644 index 0000000..1d5916c --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayFacilityRepo.java @@ -0,0 +1,41 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayFacility; + +@Repository +public interface NikshayFacilityRepo extends CrudRepository { + + @Query("select f from NikshayFacility f where f.nikshayTUID in (:tuIDs) and f.deleted = false order by f.facilityName") + List findByTUIDs(@Param("tuIDs") List tuIDs); + + @Query("select f from NikshayFacility f where f.nikshayFacilityID in (:facilityIDs) and f.deleted = false") + List findByIds(@Param("facilityIDs") List facilityIDs); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayStateRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayStateRepo.java new file mode 100644 index 0000000..7ffbf2e --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayStateRepo.java @@ -0,0 +1,37 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayState; + +@Repository +public interface NikshayStateRepo extends CrudRepository { + + @Query("select s from NikshayState s where s.deleted = false order by s.stateName") + List findAllActive(); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayTURepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayTURepo.java new file mode 100644 index 0000000..63b2313 --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayTURepo.java @@ -0,0 +1,41 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayTU; + +@Repository +public interface NikshayTURepo extends CrudRepository { + + @Query("select t from NikshayTU t where t.nikshayDistrictID = :districtID and t.deleted = false order by t.tUName") + List findByDistrictID(@Param("districtID") Integer districtID); + + @Query("select t from NikshayTU t where t.nikshayTUID in (:tuIDs) and t.deleted = false") + List findByIds(@Param("tuIDs") List tuIDs); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageFacilityMappingRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageFacilityMappingRepo.java new file mode 100644 index 0000000..55ba9b5 --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageFacilityMappingRepo.java @@ -0,0 +1,42 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayVillageFacilityMapping; + +@Repository +public interface NikshayVillageFacilityMappingRepo extends CrudRepository { + + @Query("select m from NikshayVillageFacilityMapping m where m.nikshayFacilityID in (:facilityIDs)") + List findByFacilityIDs(@Param("facilityIDs") List facilityIDs); + + // Reverse lookup: given a beneficiary's existing village ID, find their Nikshay Facility/TU + @Query("select m from NikshayVillageFacilityMapping m where m.amritVillageID = :amritVillageID") + List findByAmritVillageID(@Param("amritVillageID") Integer amritVillageID); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageRepo.java new file mode 100644 index 0000000..6f0741b --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageRepo.java @@ -0,0 +1,38 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayVillage; + +@Repository +public interface NikshayVillageRepo extends CrudRepository { + + @Query("select v from NikshayVillage v where v.nikshayFacilityID in (:facilityIDs) and v.deleted = false order by v.villageName") + List findByFacilityIDs(@Param("facilityIDs") List facilityIDs); +} diff --git a/src/main/java/com/iemr/admin/repository/rolemaster/M_UserservicerolemappingForRoleProviderAdminRepo.java b/src/main/java/com/iemr/admin/repository/rolemaster/M_UserservicerolemappingForRoleProviderAdminRepo.java index 759be90..b6937b5 100644 --- a/src/main/java/com/iemr/admin/repository/rolemaster/M_UserservicerolemappingForRoleProviderAdminRepo.java +++ b/src/main/java/com/iemr/admin/repository/rolemaster/M_UserservicerolemappingForRoleProviderAdminRepo.java @@ -49,15 +49,14 @@ public interface M_UserservicerolemappingForRoleProviderAdminRepo * serviceProviderID, Integer serviceID); */ - @Query("SELECT sm.serviceName as serviceName," + + @Query("SELECT DISTINCT sm.serviceName as serviceName," + " ssm.serviceID," + " sm.isNational as isNational," + " ssm.statusID" + " FROM M_UserservicerolemappingForRoleProviderAdmin usr" + " JOIN usr.stateServiceMapping ssm" + " JOIN ssm.serviceMaster sm" + - " WHERE usr.userID = :userID AND ssm.statusID = 2 AND usr.deleted = false" + - " GROUP BY sm.serviceName") + " WHERE usr.userID = :userID AND ssm.statusID = 2 AND usr.deleted = false") ArrayList getServiceByServiceProviderIds(@Param("userID") Integer serviceProviderID); @Query("SELECT distinct ssm.stateID," + " sm.stateName, " + " ssm.providerServiceMapID," + " ssm.statusID" diff --git a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java index d1b5a9f..4bd0012 100644 --- a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java @@ -305,7 +305,7 @@ private void saveUserUser(Employee employee, Integer row, String authorization, mUser.setTitleID(getTitleId(employee.getTitle())); mUser.setFirstName(employee.getFirstName()); mUser.setLastName(employee.getLastName()); - mUser.setUserName(employee.getContactNo()); + mUser.setUserName(employee.getUserName()); mUser.setdOB(convertStringIntoDate(employee.getDob())); mUser.setEmployeeID(employee.getUserName()); mUser.setEmergencyContactNo(String.valueOf(employee.getEmergencyContactNo())); diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java index 81b5f45..d6feae0 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java @@ -166,6 +166,9 @@ Boolean checkingEmpDetails(String userName, String aadhaarNo, String getpAN, Str // Fix 2: cascade soft-delete asha_supervisor_mapping rows when a user is deactivated void cascadeDeleteAshaMappingsForUser(Integer userID); + // Soft-delete other active mappings for same user+service, excluding the row being updated + int softDeleteOldMappings(Integer userID, Integer providerServiceMapID, Integer excludeUSRMappingID); + // Fix 2: smart cascade — for supervisor with multiple facilities, only delete mappings for this facility void cascadeDeleteAshaMappingsForDeactivation(M_UserServiceRoleMapping2 usrRole); diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java index f4c07f3..f80e4d2 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java @@ -1024,6 +1024,11 @@ public void cascadeDeleteAshaMappingsForUser(Integer userID) { ashaSupervisorMappingService.cascadeDeleteByUserID(userID, "Admin"); } + @Override + public int softDeleteOldMappings(Integer userID, Integer providerServiceMapID, Integer excludeUSRMappingID) { + return employeeMasterRepo.softDeleteOldMappings(userID, providerServiceMapID, excludeUSRMappingID); + } + @Override public void cascadeDeleteAshaMappingsForDeactivation(M_UserServiceRoleMapping2 usrRole) { Integer userID = usrRole.getUserID(); @@ -1218,8 +1223,17 @@ public ArrayList getMappedRole(Integer serviceProvider Object[] row = stateDistrictMap.get(mapping.getuSRMappingID()); if (mapping.getStateID() == null && row[1] != null) mapping.setStateID((Integer) row[1]); if (mapping.getStateName() == null && row[2] != null) mapping.setStateName((String) row[2]); - if (mapping.getWorkingDistrictID() == null && row[3] != null) mapping.setWorkingDistrictID(String.valueOf(row[3])); - if (mapping.getWorkingDistrictName() == null && row[4] != null) mapping.setWorkingDistrictName((String) row[4]); + // Stop TB's DistrictID column holds a Nikshay district ID, not an + // AMRIT one - getDirectStateDistrictByMappingIDs joins it against + // AMRIT's m_district, which for Stop TB resolves to whatever AMRIT + // district happens to share that same numeric ID (a coincidence, + // not real data - e.g. Nikshay's Angul (292) resolving to AMRIT's + // Uttar Kannad, ID 292). Skip the district patch for Stop TB only. + boolean isStopTB = "Stop TB".equals(mapping.getServiceName()); + if (!isStopTB) { + if (mapping.getWorkingDistrictID() == null && row[3] != null) mapping.setWorkingDistrictID(String.valueOf(row[3])); + if (mapping.getWorkingDistrictName() == null && row[4] != null) mapping.setWorkingDistrictName((String) row[4]); + } if (mapping.getBlockID() == null && row[5] != null) mapping.setBlockID((Integer) row[5]); if (mapping.getBlockName() == null && row[6] != null) mapping.setBlockName((String) row[6]); } diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeSignatureServiceImpl.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeSignatureServiceImpl.java index b22175c..58e5cce 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeSignatureServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeSignatureServiceImpl.java @@ -27,6 +27,7 @@ import com.iemr.admin.data.employeemaster.EmployeeSignature; import com.iemr.admin.repo.employeemaster.EmployeeSignatureRepo; +import org.json.JSONObject; @Service public class EmployeeSignatureServiceImpl implements EmployeeSignatureService { diff --git a/src/main/java/com/iemr/admin/to/employeemaster/Previleges1097_3.java b/src/main/java/com/iemr/admin/to/employeemaster/Previleges1097_3.java index d8bb0f3..4c6a757 100644 --- a/src/main/java/com/iemr/admin/to/employeemaster/Previleges1097_3.java +++ b/src/main/java/com/iemr/admin/to/employeemaster/Previleges1097_3.java @@ -34,6 +34,10 @@ public class Previleges1097_3 { private String[] villageID; private String[] villageName; private Integer facilityID; + // Comma-joined list of Nikshay TU/Facility IDs (e.g. "12,45,78"), matching + // M_UserServiceRoleMapping2's NikshayTUID/NikshayFacilityID columns. + private String nikshayTUID; + private String nikshayFacilityID; public Integer getProviderServiceMapID() { return providerServiceMapID; @@ -95,4 +99,16 @@ public Integer getFacilityID() { public void setFacilityID(Integer facilityID) { this.facilityID = facilityID; } + public String getNikshayTUID() { + return nikshayTUID; + } + public void setNikshayTUID(String nikshayTUID) { + this.nikshayTUID = nikshayTUID; + } + public String getNikshayFacilityID() { + return nikshayFacilityID; + } + public void setNikshayFacilityID(String nikshayFacilityID) { + this.nikshayFacilityID = nikshayFacilityID; + } } diff --git a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java index 2eca160..db771d1 100644 --- a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java @@ -111,7 +111,6 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } - try { String jwtFromCookie = getJwtTokenFromCookies(request); String jwtFromHeader = request.getHeader(Constants.JWT_TOKEN); From f78178cf964f00f2c42b9f232708126c74ae41f3 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 3 Aug 2026 19:45:53 +0530 Subject: [PATCH 4/8] fix: Stop TB bulk user upload district validation and qualification crash Stop TB bulk user uploads were failing 100% of the time: District validation ran against AMRIT's stale m_District table (missing post-2022 AP districts), and an unmatched Qualification silently resolved to ID 0, crashing the insert on FK_User_UserQualification instead of failing cleanly. The insert gate also only checked for missing fields, not invalid ones, so rows with invalid data still reached the DB. - Route District resolution through Nikshay's own district master (m_nikshay_district) for Stop TB service-line uploads only, via a new serviceProviderID -> ServiceID=12 check; every other service line keeps using m_District unchanged. - Add the missing "Qualification is invalid" validation check. - Fix the insert gate to check validationErrors.isEmpty() instead of a redundant chain that ignored "invalid" (non-empty but unmatched) values. --- .../M_ProviderServiceMappingRepo.java | 3 + .../BulkRegistrationServiceImpl.java | 71 +++++++++++++++++-- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/admin/repository/provideronboard/M_ProviderServiceMappingRepo.java b/src/main/java/com/iemr/admin/repository/provideronboard/M_ProviderServiceMappingRepo.java index 7cec8d2..3ba9b89 100644 --- a/src/main/java/com/iemr/admin/repository/provideronboard/M_ProviderServiceMappingRepo.java +++ b/src/main/java/com/iemr/admin/repository/provideronboard/M_ProviderServiceMappingRepo.java @@ -36,4 +36,7 @@ public interface M_ProviderServiceMappingRepo extends CrudRepository 0 FROM M_ProviderServiceMapping u WHERE u.serviceProviderID = :serviceProviderID AND u.serviceID = :serviceID AND deleted=false") + boolean existsByServiceProviderIDAndServiceID(@Param("serviceProviderID") Integer serviceProviderID, @Param("serviceID") Integer serviceID); + } diff --git a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java index 4bd0012..18568de 100644 --- a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java @@ -5,8 +5,13 @@ import com.iemr.admin.data.bulkuser.EmployeeList; import com.iemr.admin.data.employeemaster.*; import com.iemr.admin.data.locationmaster.M_District; +import com.iemr.admin.data.nikshay.NikshayDistrict; +import com.iemr.admin.data.nikshay.NikshayState; import com.iemr.admin.data.rolemaster.StateMasterForRole; import com.iemr.admin.repo.employeemaster.V_ShowuserRepo; +import com.iemr.admin.repo.nikshay.NikshayDistrictRepo; +import com.iemr.admin.repo.nikshay.NikshayStateRepo; +import com.iemr.admin.repository.provideronboard.M_ProviderServiceMappingRepo; import com.iemr.admin.service.employeemaster.EmployeeMasterInter; import com.iemr.admin.service.locationmaster.LocationMasterServiceInter; import com.iemr.admin.service.rolemaster.Role_MasterInter; @@ -60,6 +65,20 @@ public class BulkRegistrationServiceImpl implements BulkRegistrationService { @Autowired EmployeeXmlService employeeXmlService; + @Autowired + private NikshayStateRepo nikshayStateRepo; + @Autowired + private NikshayDistrictRepo nikshayDistrictRepo; + @Autowired + private M_ProviderServiceMappingRepo providerServiceMappingRepo; + + // ServiceID for "Stop TB" in m_servicemaster. Stop TB users' District is + // resolved against Nikshay's own district master (m_nikshay_district) instead + // of AMRIT's m_District, because AMRIT's district data is stale for several + // states post the 2022 district reorganizations while Nikshay's is current. + // Every other service line is unaffected and keeps using m_District as before. + private static final Integer STOP_TB_SERVICE_ID = 12; + public ArrayList errorLogs = new ArrayList<>(); public ArrayList m_bulkUser = new ArrayList<>(); public ArrayList m_UserDemographics = new ArrayList<>(); @@ -101,6 +120,7 @@ public static String escapeXmlSpecialChars(String xml) { private void saveUserUser(Employee employee, Integer row, String authorization, String createdBy, Integer serviceProviderID) throws Exception { + boolean isStopTB = providerServiceMappingRepo.existsByServiceProviderIDAndServiceID(serviceProviderID, STOP_TB_SERVICE_ID); List validationErrors = new ArrayList<>(); BulkRegistrationError bulkRegistrationErrors_ = new BulkRegistrationError(); M_User1 mUser = new M_User1(); @@ -232,6 +252,12 @@ private void saveUserUser(Employee employee, Integer row, String authorization, validationErrors.add("Qualification is missing"); } + if (!employee.getQualification().isEmpty()) { + if (getQualificationId(employee.getQualification()) == 0) { + validationErrors.add("Qualification is invalid."); + + } + } if (employee.getState().isEmpty()) { validationErrors.add("Current State is missing."); @@ -246,7 +272,9 @@ private void saveUserUser(Employee employee, Integer row, String authorization, validationErrors.add("Current District is missing."); } if (!employee.getDistrict().isEmpty()) { - if (getDistrictId(employee.getDistrict()) == 0) { + int districtId = isStopTB ? getNikshayDistrictId(employee.getState(), employee.getDistrict()) + : getDistrictId(employee.getDistrict()); + if (districtId == 0) { validationErrors.add("Current District is invalid."); } @@ -266,7 +294,9 @@ private void saveUserUser(Employee employee, Integer row, String authorization, } if (!employee.getPermanentDistrict().isEmpty()) { - if (getDistrictId(employee.getPermanentDistrict()) == 0) { + int permDistrictId = isStopTB ? getNikshayDistrictId(employee.getPermanentState(), employee.getPermanentDistrict()) + : getDistrictId(employee.getPermanentDistrict()); + if (permDistrictId == 0) { validationErrors.add("Permanent District is invalid."); } @@ -299,7 +329,7 @@ private void saveUserUser(Employee employee, Integer row, String authorization, // showLogger(employee); - if (!employee.getTitle().isEmpty() && !employee.getFirstName().isEmpty() && !employee.getLastName().isEmpty() && !employee.getContactNo().isEmpty() && !employee.getEmergencyContactNo().isEmpty() && !employee.getDob().isEmpty() && !employee.getUserName().isEmpty() && !employee.getPassword().isEmpty() && !employee.getState().isEmpty() && !employee.getDistrict().isEmpty() && !employee.getPermanentState().isEmpty() && !employee.getPermanentDistrict().isEmpty() && !employee.getGender().isEmpty() && !employee.getQualification().isEmpty() && isValidDate(convertStringIntoDate(employee.getDob()).toString()) && isValidDate(convertStringIntoDate(employee.getDateOfJoining()).toString())) { + if (validationErrors.isEmpty()) { try { mUser.setTitleID(getTitleId(employee.getTitle())); @@ -372,7 +402,9 @@ private void saveUserUser(Employee employee, Integer row, String authorization, } if (!employee.getPermanentDistrict().isEmpty()) { - mUserDemographics.setPermDistrictID(getDistrictId(employee.getPermanentDistrict())); + mUserDemographics.setPermDistrictID(isStopTB + ? getNikshayDistrictId(employee.getPermanentState(), employee.getPermanentDistrict()) + : getDistrictId(employee.getPermanentDistrict())); } mUserDemographics.setIsPermanent(false); @@ -399,7 +431,9 @@ private void saveUserUser(Employee employee, Integer row, String authorization, } mUserDemographics.setIsPresent(false); if (!employee.getDistrict().isEmpty()) { - mUserDemographics.setDistrictID(getDistrictId(employee.getDistrict())); + mUserDemographics.setDistrictID(isStopTB + ? getNikshayDistrictId(employee.getState(), employee.getDistrict()) + : getDistrictId(employee.getDistrict())); } if (!employee.getPincode().isEmpty()) { @@ -645,6 +679,33 @@ public int getDistrictId(String districtName) { } + // Stop TB-only equivalent of getDistrictId(): resolves against Nikshay's own + // district master (m_nikshay_district) instead of AMRIT's m_District, since + // Nikshay's data reflects current district boundaries (e.g. post-2022 AP + // reorganization) while m_District does not. Not used for any other service + // line. + public int getNikshayDistrictId(String stateName, String districtName) { + if (stateName == null || stateName.isEmpty() || districtName == null || districtName.isEmpty()) { + return 0; + } + + int nikshayStateId = nikshayStateRepo.findAllActive().stream() + .filter(s -> s.getStateName().equalsIgnoreCase(stateName)) + .map(NikshayState::getNikshayStateID) + .findFirst() + .orElse(0); + + if (nikshayStateId == 0) { + return 0; + } + + return nikshayDistrictRepo.findByStateID(nikshayStateId).stream() + .filter(d -> d.getDistrictName().equalsIgnoreCase(districtName)) + .map(NikshayDistrict::getNikshayDistrictID) + .findFirst() + .orElse(0); + } + public ArrayList getAllState() { return roleMasterInter.getAllState(); From 28ffe68bad5d624436e9e4178b29fa2145356386 Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:16:55 +0530 Subject: [PATCH 5/8] fix update user issue and role issue (#143) --- .../employeemaster/EmployeeMasterRepoo.java | 4 + .../employeemaster/M_DesignationRepo.java | 2 + .../BulkRegistrationServiceImpl.java | 326 +++++++++++++++++- .../employeemaster/EmployeeMasterInter.java | 6 + .../EmployeeMasterServiceImpl.java | 35 ++ 5 files changed, 371 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java index e2b017a..17819cc 100644 --- a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java +++ b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java @@ -90,4 +90,8 @@ M_User1 checkingEmpDetails(@Param("userName") String userName, @Param("aadhaarNo M_User1 findByUserID(Integer userID); List findByUserIDIn(List userIDs); + + M_User1 findEmployeeByContactForUpdate(String contactNo, Integer userID); + + M_User1 findEmployeeAadhaarNoForUpdate(String aadhaarNo, Integer userID); } diff --git a/src/main/java/com/iemr/admin/repo/employeemaster/M_DesignationRepo.java b/src/main/java/com/iemr/admin/repo/employeemaster/M_DesignationRepo.java index e141dd8..6c78708 100644 --- a/src/main/java/com/iemr/admin/repo/employeemaster/M_DesignationRepo.java +++ b/src/main/java/com/iemr/admin/repo/employeemaster/M_DesignationRepo.java @@ -22,6 +22,7 @@ package com.iemr.admin.repo.employeemaster; import java.util.ArrayList; +import java.util.List; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; @@ -35,4 +36,5 @@ public interface M_DesignationRepo extends CrudRepository getDesinationlist(); + List findByDesignationName(String designationString); } diff --git a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java index 18568de..162a470 100644 --- a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java @@ -8,6 +8,8 @@ import com.iemr.admin.data.nikshay.NikshayDistrict; import com.iemr.admin.data.nikshay.NikshayState; import com.iemr.admin.data.rolemaster.StateMasterForRole; +import com.iemr.admin.repo.employeemaster.M_DesignationRepo; +import com.iemr.admin.repo.employeemaster.M_UserDemographicsRepo; import com.iemr.admin.repo.employeemaster.V_ShowuserRepo; import com.iemr.admin.repo.nikshay.NikshayDistrictRepo; import com.iemr.admin.repo.nikshay.NikshayStateRepo; @@ -51,9 +53,16 @@ public class BulkRegistrationServiceImpl implements BulkRegistrationService { public static final String FILE_PATH = "error_log.xlsx"; // Excel file path public List bulkRegistrationErrors = new ArrayList<>(); + @Autowired + private M_UserDemographicsRepo m_UserDemographicsRepo; + @Autowired private EmployeeMasterInter employeeMasterInter; + + @Autowired + private M_DesignationRepo m_DesignationRepo; + @Autowired private Role_MasterInter roleMasterInter; @@ -95,7 +104,7 @@ public void registerBulkUser(String xml, String authorization,String userName,In logger.info("employee_list" + employeeList.getEmployees().toString()); totalEmployeeListSize = employeeList.getEmployees().size(); for (int i = 0; i < employeeList.getEmployees().size(); i++) { - saveUserUser(employeeList.getEmployees().get(i), i, authorization,userName,serviceProviderID); + processUserUser(employeeList.getEmployees().get(i), i, authorization,userName,serviceProviderID); } @@ -117,7 +126,31 @@ public static String escapeXmlSpecialChars(String xml) { // Only escape & that are not already part of valid XML entities return xml.replaceAll("&(?!amp;|lt;|gt;|apos;|quot;|#\\d+;)", "&"); } + /** + * Common entry point — decides create vs update based on whether the user already exists, + * then delegates to the respective method. + */ + private void processUserUser(Employee employee, Integer row, String authorization, String createdBy, Integer serviceProviderID) throws Exception { + if (employee.getUserName() == null || employee.getUserName().isEmpty()) { + List validationErrors = new ArrayList<>(); + BulkRegistrationError bulkRegistrationErrors_ = new BulkRegistrationError(); + validationErrors.add("Please Enter UserName"); + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + return; + } + + String checkUserIsExist = employeeMasterInter.FindEmployeeName(employee.getUserName()); + logger.info("checkUserIsExist" + checkUserIsExist); + + if (checkUserIsExist.equalsIgnoreCase("usernotexist")) { + // user not found → create flow + saveUserUser(employee, row, authorization, createdBy, serviceProviderID); + } else { + // user already exists → update flow + updateUserUser(employee, row, authorization, createdBy, serviceProviderID); + } + } private void saveUserUser(Employee employee, Integer row, String authorization, String createdBy, Integer serviceProviderID) throws Exception { boolean isStopTB = providerServiceMappingRepo.existsByServiceProviderIDAndServiceID(serviceProviderID, STOP_TB_SERVICE_ID); @@ -504,6 +537,291 @@ private void saveUserUser(Employee employee, Integer row, String authorization, */ + private void updateUserUser(Employee employee, Integer row, String authorization, String modifiedBy, Integer serviceProviderID) throws Exception { + boolean isStopTB = providerServiceMappingRepo.existsByServiceProviderIDAndServiceID(serviceProviderID, STOP_TB_SERVICE_ID); + + List validationErrors = new ArrayList<>(); + BulkRegistrationError bulkRegistrationErrors_ = new BulkRegistrationError(); + + logger.info("employee_list update flow" + employee.toString()); + + // fetch existing user record to update + M_User1 existingUser = employeeMasterInter.FindEmployeeName1(employee.getUserName()); + if (existingUser == null) { + validationErrors.add("User not found for update"); + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + return; + } + Integer userID = existingUser.getUserID(); + + // duplicate contact check excluding current user + String checkContactIsExist = employeeMasterInter.FindEmployeeContactForUpdate(employee.getContactNo(), userID); + + if (checkContactIsExist.equalsIgnoreCase("contactnotexist")) { + + if (employee.getTitle() == null || employee.getTitle().isEmpty()) { + validationErrors.add("Title is missing."); + } + if (!employee.getTitle().isEmpty() && getTitleId(employee.getTitle()) == 0) { + validationErrors.add("Title is invalid."); + } + + if (employee.getFirstName() == null || employee.getFirstName().isEmpty()) { + validationErrors.add("First Name is missing."); + } + if (!employee.getFirstName().isEmpty()) { + if (employee.getFirstName().length() > 50) { + validationErrors.add("First name is invalid."); + } + if (isNumeric(employee.getFirstName())) { + validationErrors.add("First name is invalid."); + } + } + + if (!employee.getMiddleName().isEmpty()) { + if (employee.getMiddleName().length() > 50) { + validationErrors.add("Middle name is invalid."); + } + if (isNumeric(employee.getMiddleName())) { + validationErrors.add("Middle name is invalid."); + } + } + + if (employee.getLastName() == null || employee.getLastName().isEmpty()) { + validationErrors.add("Last Name is missing."); + } + if (!employee.getLastName().isEmpty()) { + if (employee.getLastName().length() > 50) { + validationErrors.add("Last name is invalid."); + } + if (isNumeric(employee.getLastName())) { + validationErrors.add("Last name is invalid."); + } + } + + if (employee.getGender().isEmpty()) { + validationErrors.add("Gender is missing"); + } + if (employee.getContactNo().isEmpty()) { + validationErrors.add("Contact number missing"); + } + if (!employee.getContactNo().isEmpty() && !isValidPhoneNumber(String.valueOf(employee.getContactNo()))) { + validationErrors.add("Contact Number is invalid"); + } + + if (employee.getDesignation().isEmpty()) { + validationErrors.add("Designation is missing"); + } + + if (employee.getEmergencyContactNo().isEmpty()) { + validationErrors.add("Emergency contact number is missing"); + } + if (!employee.getEmergencyContactNo().isEmpty() && !isValidPhoneNumber(String.valueOf(employee.getEmergencyContactNo()))) { + validationErrors.add("Emergency Contact Number is invalid."); + } + + if (employee.getDob().isEmpty()) { + validationErrors.add("Date of Birth is missing."); + } + if (!employee.getDob().isEmpty() && !isValidDate(convertStringIntoDate(employee.getDob()).toString())) { + validationErrors.add("Date of Birth is invalid."); + } + + if (employee.getEmail().isEmpty()) { + validationErrors.add("Email is missing."); + } + if (!employee.getEmail().isEmpty() && !employee.getEmail().matches(EMAIL_REGEX)) { + validationErrors.add("Invalid Email format."); + } + + // password optional on update — only validated if provided + if (!employee.getPassword().isEmpty() && employee.getPassword().length() < 6) { + validationErrors.add("Please enter a valid password."); + } + + if (!employee.getAadhaarNo().isEmpty()) { + String checkAadhaarIsExist = employeeMasterInter.FindEmployeeAadhaarForUpdate(employee.getAadhaarNo(), userID); + if (!checkAadhaarIsExist.equalsIgnoreCase("aadhaarnotexist")) { + validationErrors.add("Duplicate aadhaar number found"); + } + if (isValidAadhar(employee.getAadhaarNo())) { + validationErrors.add("Aadhaar number is invalid"); + } + } + + if (employee.getQualification().isEmpty()) { + validationErrors.add("Qualification is missing"); + } + + if (employee.getState().isEmpty()) { + validationErrors.add("Current State is missing."); + } + if (!employee.getState().isEmpty() && getStateId(employee.getState()) == 0) { + validationErrors.add("Current State is invalid."); + } + + if (employee.getDistrict().isEmpty()) { + validationErrors.add("Current District is missing."); + } + if (!employee.getDistrict().isEmpty() && getDistrictId(employee.getDistrict()) == 0) { + validationErrors.add("Current District is invalid."); + } + + if (employee.getPermanentState().isEmpty()) { + validationErrors.add("Permanent State is missing."); + } + if (!employee.getPermanentState().isEmpty() && getStateId(employee.getPermanentState()) == 0) { + validationErrors.add("Permanent State is invalid."); + } + + if (employee.getPermanentDistrict().isEmpty()) { + validationErrors.add("Permanent District is missing."); + } + if (!employee.getPermanentDistrict().isEmpty() && getDistrictId(employee.getPermanentDistrict()) == 0) { + validationErrors.add("Permanent District is invalid."); + } + + if (employee.getDateOfJoining().isEmpty()) { + validationErrors.add("Date of Joining is missing."); + } + if (!employee.getDateOfJoining().isEmpty() && !isValidDate(convertStringIntoDate(employee.getDateOfJoining()).toString())) { + validationErrors.add("Date of Joining is invalid."); + } + + if (!validationErrors.isEmpty()) { + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + } + + boolean coreFieldsValid = !employee.getTitle().isEmpty() && !employee.getFirstName().isEmpty() + && !employee.getLastName().isEmpty() && !employee.getContactNo().isEmpty() + && !employee.getEmergencyContactNo().isEmpty() && !employee.getDob().isEmpty() + && !employee.getUserName().isEmpty() && !employee.getState().isEmpty() + && !employee.getDistrict().isEmpty() && !employee.getPermanentState().isEmpty() + && !employee.getPermanentDistrict().isEmpty() && !employee.getGender().isEmpty() + && !employee.getQualification().isEmpty() + && isValidDate(convertStringIntoDate(employee.getDob()).toString()) + && isValidDate(convertStringIntoDate(employee.getDateOfJoining()).toString()); + + if (coreFieldsValid) { + try { + M_User1 mUser = existingUser; + mUser.setUserID(mUser.getUserID()); + mUser.setTitleID(getTitleId(employee.getTitle())); + mUser.setFirstName(employee.getFirstName()); + mUser.setLastName(employee.getLastName()); + mUser.setdOB(convertStringIntoDate(employee.getDob())); + mUser.setEmergencyContactNo(String.valueOf(employee.getEmergencyContactNo())); + mUser.setContactNo(String.valueOf(employee.getContactNo())); + + if (!employee.getMiddleName().isEmpty()) { + mUser.setMiddleName(employee.getMiddleName()); + } + if (!employee.getDesignation().isEmpty()) { + mUser.setDesignationID(getDesignationId(employee.getDesignation())); + mUser.setDesignationName(employee.getDesignation()); + } + if (!employee.getAadhaarNo().isEmpty() && isValidAadhar(employee.getAadhaarNo())) { + mUser.setAadhaarNo(String.valueOf(employee.getAadhaarNo())); + } + if (!employee.getPan().isEmpty()) { + mUser.setpAN(employee.getPan()); + } + mUser.setEmailID(employee.getEmail()); + mUser.setGenderID(Short.parseShort(String.valueOf(getGenderId(employee.getGender())))); + if (!employee.getQualification().isEmpty()) { + mUser.setQualificationID(getQualificationId(employee.getQualification())); + } + mUser.setdOJ(convertStringIntoDate(employee.getDateOfJoining())); + mUser.setModifiedBy(modifiedBy); + mUser.setServiceProviderID(serviceProviderID); + + // password touched only if a new one was supplied + if (!employee.getPassword().isEmpty()) { + mUser.setPassword(generateStrongPassword(employee.getPassword())); + } + + M_User1 updatedUser = employeeMasterInter.saveBulkUserEmployee(mUser); + + M_UserDemographics mUserDemographics = employeeMasterInter.getUserDemographicsByUserID(updatedUser.getUserID()); + if (mUserDemographics == null) { + mUserDemographics = new M_UserDemographics(); + mUserDemographics.setUserID(updatedUser.getUserID()); + mUserDemographics.setCountryID(91); + } + + if (!employee.getCommunity().isEmpty()) { + mUserDemographics.setCommunityID(getCommunityId(employee.getCommunity())); + } + if (!employee.getReligion().isEmpty()) { + mUserDemographics.setReligionID(getReligionStringId(employee.getReligion())); + } + mUserDemographics.setModifiedBy(modifiedBy); + + if (!employee.getPermanentAddressLine1().isEmpty()) { + mUserDemographics.setPermAddressLine1(employee.getPermanentAddressLine1()); + } + if (!employee.getPermanentState().isEmpty()) { + mUserDemographics.setPermStateID(getStateId(employee.getPermanentState())); + } + if (!employee.getPermanentDistrict().isEmpty()) { + mUserDemographics.setPermDistrictID(getDistrictId(employee.getPermanentDistrict())); + } + if (!employee.getPermanentPincode().isEmpty()) { + mUserDemographics.setPermPinCode(Integer.valueOf(employee.getPermanentPincode())); + } + if (!employee.getMotherName().isEmpty()) { + mUserDemographics.setMothersName(employee.getMotherName()); + } + if (!employee.getFatherName().isEmpty()) { + mUserDemographics.setFathersName(employee.getFatherName()); + } + if (!employee.getAddressLine1().isEmpty()) { + mUserDemographics.setAddressLine1(employee.getAddressLine1()); + } + if (!employee.getState().isEmpty()) { + mUserDemographics.setStateID(getStateId(employee.getState())); + } + if (!employee.getDistrict().isEmpty()) { + int districtId = isStopTB ? getNikshayDistrictId(employee.getState(), employee.getDistrict()) + : getDistrictId(employee.getDistrict()); + if (districtId == 0) { + validationErrors.add("Current District is invalid."); + + } + } + if (!employee.getPincode().isEmpty()) { + mUserDemographics.setPinCode(employee.getPincode().toString()); + } + + m_UserDemographicsRepo.save(mUserDemographics); + + m_bulkUser.add(mUser); + m_UserDemographics.add(mUserDemographics); + + } catch (Exception e) { + errorLogs.add("Row :" + (row + 1) + e.getMessage()); + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + } + } + + } else { + validationErrors.add("Contact No already belongs to another user"); + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + } + } + + // shared helper — same duplicated block ko replace kar diya + private void logAndCollectError(Integer row, Employee employee, List validationErrors, + BulkRegistrationError bulkRegistrationErrors_) { + if (!validationErrors.isEmpty()) { + errorLogs.add("Row " + (row + 1) + ": " + String.join(", ", validationErrors)); + bulkRegistrationErrors_.setRowNumber((row + 1)); + bulkRegistrationErrors_.setUserName(employee.getUserName()); + bulkRegistrationErrors_.setError(validationErrors); + bulkRegistrationErrors.add(bulkRegistrationErrors_); + } + } + private boolean isValidDate(String dateStr) { try { String[] parts = dateStr.split("-"); @@ -621,8 +939,12 @@ private int getTitleId(String titleString) { public int getDesignationId(String designationString) { + if(!m_DesignationRepo.findByDesignationName(designationString).isEmpty()){ + return m_DesignationRepo.findByDesignationName(designationString).get(0).getDesignationID(); - return 20; + }else { + return 0; + } } diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java index d6feae0..859f087 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java @@ -201,4 +201,10 @@ Boolean checkingEmpDetails(String userName, String aadhaarNo, String getpAN, Str M_UserServiceRoleMapping2 deleteuserrolemapTM(M_UserServiceRoleMapping2 pre) throws Exception; M_User1 saveBulkUserEmployee(M_User1 mUser); + + String FindEmployeeContactForUpdate(String contactNo, Integer userID); + + String FindEmployeeAadhaarForUpdate(String aadhaarNo, Integer userID); + + M_UserDemographics getUserDemographicsByUserID(Integer userID); } diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java index f80e4d2..f09d0bd 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java @@ -1508,4 +1508,39 @@ public M_User1 saveBulkUserEmployee(M_User1 mUser) { return data; } + @Override + public String FindEmployeeContactForUpdate(String contactNo, Integer userID) { + logger.info("EmployeeMasterServiceImpl.FindEmployeeContactForUpdate - start"); + M_User1 data = employeeMasterRepo11.findEmployeeByContactForUpdate(contactNo, userID); + String result; + if (data == null) { + result = "contactnotexist"; + } else { + result = "contactexist"; + } + logger.info("EmployeeMasterServiceImpl.FindEmployeeContactForUpdate - finish"); + return result; + } + + @Override + public String FindEmployeeAadhaarForUpdate(String aadhaarNo, Integer userID) { + logger.info("EmployeeMasterServiceImpl.FindEmployeeAadhaarForUpdate - start"); + M_User1 data = employeeMasterRepo11.findEmployeeAadhaarNoForUpdate(aadhaarNo, userID); + String result; + if (data == null) { + result = "aadhaarnotexist"; + } else { + result = "aadhaarexist"; + } + logger.info("EmployeeMasterServiceImpl.FindEmployeeAadhaarForUpdate - finish"); + return result; + } + + @Override + public M_UserDemographics getUserDemographicsByUserID(Integer userID) { + logger.info("EmployeeMasterServiceImpl.getUserDemographicsByUserID - start"); + M_UserDemographics data = m_UserDemographicsRepo.findByUserID(userID); + logger.info("EmployeeMasterServiceImpl.getUserDemographicsByUserID - finish"); + return data; + } } From c9e1d851798790ec0a8fecc0ecdf073fa571cfda Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:52:52 +0530 Subject: [PATCH 6/8] Fix/admin bulk registration role issue (#144) * fix update user issue and role issue * fix update user issue and role issue --- .../iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java index 17819cc..74fe406 100644 --- a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java +++ b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java @@ -91,7 +91,9 @@ M_User1 checkingEmpDetails(@Param("userName") String userName, @Param("aadhaarNo List findByUserIDIn(List userIDs); - M_User1 findEmployeeByContactForUpdate(String contactNo, Integer userID); + @Query("SELECT u FROM M_User1 u WHERE u.contactNo=:contactNo AND u.userID<>:userID AND deleted=false") + M_User1 findEmployeeByContactForUpdate(@Param("contactNo") String contactNo, @Param("userID") Integer userID); - M_User1 findEmployeeAadhaarNoForUpdate(String aadhaarNo, Integer userID); + @Query("SELECT u FROM M_User1 u WHERE u.aadhaarNo=:aadhaar AND u.userID<>:userID AND deleted=false") + M_User1 findEmployeeAadhaarNoForUpdate(@Param("aadhaar") String aadhaar, @Param("userID") Integer userID); } From c6ada0ae1333edbc3d1ec5fda2eb9c431f21ff5f Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:37:33 +0530 Subject: [PATCH 7/8] USER ID / Login ID Update for Mobile Number Change (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(employee-master): validate username uniqueness on edit, stop clearing Employee ID Edit no longer silently nulls Employee ID on every save — the edit form doesn't submit that field, and the code was unconditionally copying the (missing) value over the existing one. Also checks the new username isn't already taken by another employee before saving, rather than allowing silent duplicates. * feat(admin): add username rename API with audit trail propagation CreatedBy/ModifiedBy across AMRIT store the username as a denormalised string rather than a UserID foreign key, so renaming a user has to be propagated by hand. Adds POST /username/renameUsername plus a /preview variant that reports the same per-table row counts without committing. The sweep covers m_user and 28 RMNCH/FLW tables across db_iemr and db_identity, in one transaction so a partial rename cannot be left behind. Column naming was verified against the AMRIT-DB migrations: db_identity is uniformly CreatedBy/ModifiedBy while db_iemr is split between created_by/updated_by and CreatedBy/ModifiedBy. Each table takes two UPDATEs, one per audit column, rather than a single combined statement. Setting both columns off a CreatedBy match would overwrite ModifiedBy on rows a different user last touched, destroying their attribution, and would miss rows this user only modified. Usernames are bound as parameters; only whitelist constants are interpolated. m_user is updated first so a unique-key rejection on UserName or EmployeeID happens before anything else is written, and the new name is capped at 12 characters when contact numbers are included because m_user.ContactNo is varchar(12). Scope is deliberately these tables only — it is not every table carrying an audit column, so the old username survives elsewhere. Co-Authored-By: Claude Opus 5 * fix(admin): make username rename preview index-friendly and exact Preview timed out behind nginx on UAT (504). Each of the 28 tables was counted with `createdBy = :u OR modifiedBy = :u`, and an OR across two columns cannot use a single-column index on either — so every table was a full scan. Split into two single-column counts, matching the two UPDATEs the rename already issues, so both can use the new indexes in AMRIT-DB V96/V19. Splitting also makes the numbers honest: a row the user both created and last modified is updated by each statement, so counting it once under-reported what rename() goes on to report. Preview now also counts m_user, which it previously skipped while the rename reported it, so the two line up row for row. Corrects the ELIGIBLE_COUPLE_TRACKING entry to eligible_couple_tracking — the schema creates it lowercase, and table names are case-sensitive on Linux MySQL. Co-Authored-By: Claude Opus 5 * Revert "fix(admin): make username rename preview index-friendly and exact" This reverts commit cd20896e681521ac3e5582208fdcf21659d3516a. * refactor(admin): drive username rename updates by primary key Reworks the sweep to the supplied query shape: each UPDATE now selects the rows to touch by primary key through a derived table rather than filtering on the audit column directly. The SELECT ... AS temp wrapper is what MySQL requires to read from the same table an UPDATE targets (error 1093), and driving by PK keeps the statements usable under safe-update mode. Carries each table's primary key in the registry; all 28 were verified against the AMRIT-DB migrations. Both audit columns are now set off a CreatedBy match, per the supplied queries, replacing the earlier split into one statement per column. Preview counts CreatedBy alone to match, and still reports m_user, so preview and result line up row for row. Keeps eligible_couple_tracking lower case: the schema creates it that way and MySQL table names are case sensitive on Linux. Co-Authored-By: Claude Opus 5 * feat(admin): drop the username rename preview endpoint Removes POST /username/renameUsername/preview along with the service method behind it, the two counting queries it used, and the now-unused preview flag on the response. /username/renameUsername is unchanged and remains the only endpoint. Co-Authored-By: Claude Opus 5 * feat(admin): accept an optional new Employee ID on username rename EmployeeID was hardcoded to the new username in both branches of the m_user update. It is now only touched when the request supplies a new value, so renaming a username no longer implies changing the Employee ID. The SET clause is built from what was actually asked for. Splits the uniqueness check in two. UserName and EmployeeID hold separate UNIQUE keys on m_user, so each conflicts only with its own column — the previous combined check rejected a new username merely because some row used it as an Employee ID, which was only correct while the two were forced to match. Both checks exclude the row being renamed, so re-entering the value that row already holds is not reported as a conflict with itself. Co-Authored-By: Claude Opus 5 * feat(admin): add username/employee ID availability endpoint Adds POST /username/checkAvailability so the screen can report a duplicate as it is typed rather than at submit. It reuses the same repository checks rename() runs, so the two cannot disagree — including counting soft-deleted rows, which still hold m_user's UNIQUE keys. Co-Authored-By: Claude Opus 5 * feat(admin): update username and employee ID independently Both new values are now optional. Each is normalised to null when blank or already equal to what the row holds, and the m_user UPDATE is built from whichever columns actually change — so a username-only change, an employee-ID-only change, or both are all valid. The request is rejected only when neither would change anything. The 28-table audit sweep is skipped entirely when the username is unchanged: CreatedBy/ModifiedBy record the username, so an employee-ID-only change leaves every audit row already correct. That also keeps such a change off the unindexed scan path. Drops the updateEmployeeId flag (supplying a value is the intent) and the /username/checkAvailability endpoint, since the screen now uses the same m/FindEmployeeByName and m/FindEmployeeDetails checks as Employee Master. Co-Authored-By: Claude Opus 5 * feat(admin): address the rename target by userID Takes userID on the rename request and uses it as the identity of the row being changed. The username is the very thing being renamed, so a string match is a poor way to address it; the primary key is not. The m_user UPDATE is now keyed on UserID, and both uniqueness checks exclude self by UserID rather than by username — the same way the Employee Master edit path does it via findEmployeeByNameForUpdate. The old username is now read from the row rather than trusted from the request, and a mismatch is rejected: if the row was renamed after the screen loaded its list, sweeping on the stale username would repoint the wrong audit records. Co-Authored-By: Claude Opus 5 * fix(admin): return the employee ID in the rename response The response carried only the username pair, so a caller had no way to see what happened to the employee ID. Adds oldEmployeeId (read before the update) and newEmployeeId. Also adds userNameUpdated/employeeIdUpdated booleans. OutputResponse re-serialises the envelope with a GsonBuilder that has no serializeNulls, so any null field drops out of the JSON entirely — which is why an unset employee ID appears to be missing rather than null. A primitive boolean always survives that pass, so the caller can tell "unchanged" from "absent". Co-Authored-By: Claude Opus 5 * feat(admin): drop row/table counts from the rename response Removes rowsPerTable, tablesAffected and totalRowsAffected. The response now reports only what changed: the username and employee ID before and after, plus the flags saying whether each was written. The total row count is still summed and logged, so the scale of a rename remains traceable without exposing the table list to callers. Co-Authored-By: Claude Opus 5 * Revert "fix(employee-master): validate username uniqueness on edit, stop clearing Employee ID" This reverts commit 4f6371f061910a5cc951e105dd0ff34f3a57b278. * revert: undo the employee edit page username/Employee ID changes Reverts 4f6371f — the editUserDetails uniqueness check, the Employee ID null-guard and findEmployeeByNameForUpdate. Verified independent of the Change Username feature: nothing in the username package called any of it, so the rename API is unaffected. Drops the comment that pointed at the removed query. Note this restores the prior editUserDetails behaviour, including the unconditional setEmployeeID that nulls Employee ID when the edit form does not submit that field. Co-Authored-By: Claude Opus 5 * style(admin): remove comments from the username rename classes Pure deletions — no code changed. GPL license headers kept. Co-Authored-By: Claude Opus 5 * fix(admin): address the scan findings on the username rename Log injection (UsernameRenameServiceImpl): the rename start line logged the old/new username and employee ID straight from the request, so a value containing CR/LF could forge log entries. Those values are now flattened to a safe character set and length-capped before logging, and the line also carries the userID, which is an integer and cannot be tampered with. Dynamic SQL (UsernameRenameRepository): schema, table and column names cannot be bound as parameters, so they are interpolated. Each is now checked against a strict SQL-identifier pattern before it can reach a statement, so anything that is not a plain identifier throws instead of being concatenated in. All caller-supplied data was, and remains, bound. Verified every identifier in the table registry satisfies the pattern. Co-Authored-By: Claude Opus 5 * fix(admin): stop logging request-derived strings on rename The sanitising helper did not clear Sonar's taint analysis — a custom regex replace is not recognised as a sanitiser, so the rule still traced request data into the logger and the Quality Gate stayed at Security Rating C. Logs the userID and two booleans saying which columns are changing instead. Nothing request-derived reaches the log, so the taint path is gone rather than filtered. The values themselves are still available in the response and in m_user. Co-Authored-By: Claude Opus 5 * fix(admin): build rename SQL from constants, not formatted strings Sonar reported three MAJOR vulnerabilities for dynamically formatted SQL in UsernameRenameRepository, holding New Code at Security Rating C and failing the Quality Gate. Each query call now receives a constant rather than a formatted string: - the two uniqueness counts become two fixed queries instead of one with an interpolated column name - the m_user update selects one of five constants covering the possible combinations, instead of joining a SET clause at runtime - the per-table rename SQL is built once in AuditTable's constructor and read back through a getter, so the query call takes a plain value Identifier validation moves to AuditTable, where the identifiers now live, and rejects anything that is not a plain SQL identifier. The generated SQL is unchanged: verified byte-identical for the per-table rename and for all eight combinations of the m_user update. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Sneha Co-authored-by: Claude Opus 5 --- .../username/UsernameRenameController.java | 64 +++++++ .../model/username/UsernameRenameRequest.java | 73 ++++++++ .../username/UsernameRenameResponse.java | 81 +++++++++ .../username/UsernameAuditTables.java | 120 +++++++++++++ .../username/UsernameRenameRepository.java | 143 ++++++++++++++++ .../username/UsernameRenameService.java | 29 ++++ .../username/UsernameRenameServiceImpl.java | 158 ++++++++++++++++++ 7 files changed, 668 insertions(+) create mode 100644 src/main/java/com/iemr/admin/controller/username/UsernameRenameController.java create mode 100644 src/main/java/com/iemr/admin/model/username/UsernameRenameRequest.java create mode 100644 src/main/java/com/iemr/admin/model/username/UsernameRenameResponse.java create mode 100644 src/main/java/com/iemr/admin/repository/username/UsernameAuditTables.java create mode 100644 src/main/java/com/iemr/admin/repository/username/UsernameRenameRepository.java create mode 100644 src/main/java/com/iemr/admin/service/username/UsernameRenameService.java create mode 100644 src/main/java/com/iemr/admin/service/username/UsernameRenameServiceImpl.java diff --git a/src/main/java/com/iemr/admin/controller/username/UsernameRenameController.java b/src/main/java/com/iemr/admin/controller/username/UsernameRenameController.java new file mode 100644 index 0000000..907311f --- /dev/null +++ b/src/main/java/com/iemr/admin/controller/username/UsernameRenameController.java @@ -0,0 +1,64 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.controller.username; + +import javax.ws.rs.core.MediaType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.iemr.admin.model.username.UsernameRenameRequest; +import com.iemr.admin.service.username.UsernameRenameService; +import com.iemr.admin.utils.mapper.OutputMapper; +import com.iemr.admin.utils.response.OutputResponse; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping(value = "/username") +public class UsernameRenameController { + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + @Autowired + private UsernameRenameService usernameRenameService; + + @Operation(summary = "Rename a username and repoint its audit records") + @RequestMapping(value = "/renameUsername", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String renameUsername(@RequestBody UsernameRenameRequest renameRequest, HttpServletRequest request) { + OutputResponse response = new OutputResponse(); + try { + logger.info("renameUsername received request"); + response.setResponse(OutputMapper.gsonWithoutExpose().toJson(usernameRenameService.rename(renameRequest))); + } catch (Exception e) { + logger.error("renameUsername failed", e); + response.setError(e); + } + logger.info("renameUsername sending response"); + return response.toString(); + } +} diff --git a/src/main/java/com/iemr/admin/model/username/UsernameRenameRequest.java b/src/main/java/com/iemr/admin/model/username/UsernameRenameRequest.java new file mode 100644 index 0000000..b0f4998 --- /dev/null +++ b/src/main/java/com/iemr/admin/model/username/UsernameRenameRequest.java @@ -0,0 +1,73 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.model.username; + +public class UsernameRenameRequest { + private Integer userID; + + private String oldUserName; + private String newUserName; + + private String newEmployeeId; + + private boolean updateContactFields = true; + + public Integer getUserID() { + return userID; + } + + public void setUserID(Integer userID) { + this.userID = userID; + } + + public String getOldUserName() { + return oldUserName; + } + + public void setOldUserName(String oldUserName) { + this.oldUserName = oldUserName; + } + + public String getNewUserName() { + return newUserName; + } + + public void setNewUserName(String newUserName) { + this.newUserName = newUserName; + } + + public String getNewEmployeeId() { + return newEmployeeId; + } + + public void setNewEmployeeId(String newEmployeeId) { + this.newEmployeeId = newEmployeeId; + } + + public boolean isUpdateContactFields() { + return updateContactFields; + } + + public void setUpdateContactFields(boolean updateContactFields) { + this.updateContactFields = updateContactFields; + } +} diff --git a/src/main/java/com/iemr/admin/model/username/UsernameRenameResponse.java b/src/main/java/com/iemr/admin/model/username/UsernameRenameResponse.java new file mode 100644 index 0000000..993f749 --- /dev/null +++ b/src/main/java/com/iemr/admin/model/username/UsernameRenameResponse.java @@ -0,0 +1,81 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.model.username; + +public class UsernameRenameResponse { + private String oldUserName; + private String newUserName; + + private String oldEmployeeId; + private String newEmployeeId; + + private boolean userNameUpdated; + private boolean employeeIdUpdated; + + public boolean isUserNameUpdated() { + return userNameUpdated; + } + + public void setUserNameUpdated(boolean userNameUpdated) { + this.userNameUpdated = userNameUpdated; + } + + public boolean isEmployeeIdUpdated() { + return employeeIdUpdated; + } + + public void setEmployeeIdUpdated(boolean employeeIdUpdated) { + this.employeeIdUpdated = employeeIdUpdated; + } + + public String getOldEmployeeId() { + return oldEmployeeId; + } + + public void setOldEmployeeId(String oldEmployeeId) { + this.oldEmployeeId = oldEmployeeId; + } + + public String getNewEmployeeId() { + return newEmployeeId; + } + + public void setNewEmployeeId(String newEmployeeId) { + this.newEmployeeId = newEmployeeId; + } + + public String getOldUserName() { + return oldUserName; + } + + public void setOldUserName(String oldUserName) { + this.oldUserName = oldUserName; + } + + public String getNewUserName() { + return newUserName; + } + + public void setNewUserName(String newUserName) { + this.newUserName = newUserName; + } +} diff --git a/src/main/java/com/iemr/admin/repository/username/UsernameAuditTables.java b/src/main/java/com/iemr/admin/repository/username/UsernameAuditTables.java new file mode 100644 index 0000000..7afffac --- /dev/null +++ b/src/main/java/com/iemr/admin/repository/username/UsernameAuditTables.java @@ -0,0 +1,120 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.repository.username; + +import java.util.List; +import java.util.regex.Pattern; + +public final class UsernameAuditTables { + private UsernameAuditTables() { + } + + private static final Pattern SQL_IDENTIFIER = Pattern.compile("\\w+(\\.\\w+)?"); + + private static String identifier(String name) { + if (name == null || !SQL_IDENTIFIER.matcher(name).matches()) { + throw new IllegalArgumentException("Illegal SQL identifier: " + name); + } + return name; + } + + public static final class AuditTable { + private final String qualifiedName; + private final String createdByColumn; + private final String modifiedByColumn; + private final String primaryKeyColumn; + private final String renameSql; + + public AuditTable(String qualifiedName, String createdByColumn, String modifiedByColumn, + String primaryKeyColumn) { + this.qualifiedName = identifier(qualifiedName); + this.createdByColumn = identifier(createdByColumn); + this.modifiedByColumn = identifier(modifiedByColumn); + this.primaryKeyColumn = identifier(primaryKeyColumn); + this.renameSql = String.format( + "UPDATE %1$s SET %2$s = :newUserName, %3$s = :newUserName " + + "WHERE %4$s IN (SELECT %4$s FROM (SELECT %4$s FROM %1$s WHERE %2$s = :oldUserName)" + + " AS temp)", + this.qualifiedName, this.createdByColumn, this.modifiedByColumn, this.primaryKeyColumn); + } + + public String getRenameSql() { + return renameSql; + } + + public String getQualifiedName() { + return qualifiedName; + } + + public String getCreatedByColumn() { + return createdByColumn; + } + + public String getModifiedByColumn() { + return modifiedByColumn; + } + + public String getPrimaryKeyColumn() { + return primaryKeyColumn; + } + } + + private static AuditTable pascal(String qualifiedName, String primaryKeyColumn) { + return new AuditTable(qualifiedName, "CreatedBy", "ModifiedBy", primaryKeyColumn); + } + + private static AuditTable snake(String qualifiedName, String primaryKeyColumn) { + return new AuditTable(qualifiedName, "created_by", "updated_by", primaryKeyColumn); + } + + public static final List TABLES = List.of( + pascal("db_identity.i_beneficiarydetails_rmnch", "beneficiaryDetails_RmnchId"), + pascal("db_identity.i_beneficiaryfamilymapping", "BenFamilyMapId"), + pascal("db_identity.i_beneficiarydetails", "BeneficiaryDetailsId"), + pascal("db_identity.i_beneficiarymapping", "BenMapId"), + pascal("db_identity.i_beneficiaryidentity", "BenIdentityId"), + pascal("db_identity.i_householddetails", "houseHoldDetailsId"), + pascal("db_identity.i_beneficiaryimage", "BenImageId"), + pascal("db_identity.i_beneficiaryaddress", "BenAddressID"), + pascal("db_identity.i_beneficiaryservicemapping", "BenServiceMapID"), + pascal("db_identity.m_beneficiaryregidmapping", "BenRegId"), + pascal("db_identity.i_bornbirthdeatils", "BornBirthDeatilsId"), + pascal("db_identity.i_beneficiarycontacts", "BenContactsID"), + pascal("db_identity.i_beneficiaryconsent", "BenConsentID"), + pascal("db_identity.i_benfamilytag", "BenFamilyTagId"), + + snake("db_iemr.eligible_couple_tracking", "id"), + snake("db_iemr.t_pregnant_woman_register", "id"), + snake("db_iemr.t_eligible_couple_register", "id"), + snake("db_iemr.t_delivery_outcome", "id"), + snake("db_iemr.t_infant_register", "id"), + snake("db_iemr.t_pnc_visit", "ID"), + snake("db_iemr.t_anc_visit", "ID"), + snake("db_iemr.t_child_register", "ID"), + snake("db_iemr.t_pmsma", "id"), + + pascal("db_iemr.t_cbacdetails", "id"), + pascal("db_iemr.t_pnccare", "id"), + pascal("db_iemr.t_anccare", "ID"), + pascal("db_iemr.t_benvisitdetail", "BenVisitID"), + pascal("db_iemr.t_childvaccinedetail1", "ID")); +} diff --git a/src/main/java/com/iemr/admin/repository/username/UsernameRenameRepository.java b/src/main/java/com/iemr/admin/repository/username/UsernameRenameRepository.java new file mode 100644 index 0000000..e0a3088 --- /dev/null +++ b/src/main/java/com/iemr/admin/repository/username/UsernameRenameRepository.java @@ -0,0 +1,143 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.repository.username; + +import java.math.BigInteger; +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.iemr.admin.repository.username.UsernameAuditTables.AuditTable; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; + +@Repository +public class UsernameRenameRepository { + /** + * Schema, table and column names cannot be bound as parameters, so they are + * interpolated. Every identifier is checked against this before it reaches a + * statement, so a value that is not a plain SQL identifier can never be + * concatenated in. All caller-supplied data is bound, never interpolated. + */ + private static final String COUNT_BY_USER_NAME = + "SELECT COUNT(*) FROM db_iemr.m_user WHERE UserName = :value AND UserID <> :excludeUserID"; + + private static final String COUNT_BY_EMPLOYEE_ID = + "SELECT COUNT(*) FROM db_iemr.m_user WHERE EmployeeID = :value AND UserID <> :excludeUserID"; + + private static final String SET_USER_NAME = + "UPDATE db_iemr.m_user SET UserName = :newUserName WHERE UserID = :userID"; + + private static final String SET_USER_NAME_AND_CONTACTS = + "UPDATE db_iemr.m_user SET UserName = :newUserName, EmergencyContactNo = :newUserName," + + " ContactNo = :newUserName WHERE UserID = :userID"; + + private static final String SET_EMPLOYEE_ID = + "UPDATE db_iemr.m_user SET EmployeeID = :newEmployeeId WHERE UserID = :userID"; + + private static final String SET_USER_NAME_AND_EMPLOYEE_ID = + "UPDATE db_iemr.m_user SET UserName = :newUserName, EmployeeID = :newEmployeeId WHERE UserID = :userID"; + + private static final String SET_USER_NAME_CONTACTS_AND_EMPLOYEE_ID = + "UPDATE db_iemr.m_user SET UserName = :newUserName, EmergencyContactNo = :newUserName," + + " ContactNo = :newUserName, EmployeeID = :newEmployeeId WHERE UserID = :userID"; + + @PersistenceContext + private EntityManager entityManager; + + public long renameInTable(AuditTable table, String oldUserName, String newUserName) { + Query query = entityManager.createNativeQuery(table.getRenameSql()); + query.setParameter("newUserName", newUserName); + query.setParameter("oldUserName", oldUserName); + return query.executeUpdate(); + } + + public String currentUserName(Integer userID) { + return single("SELECT UserName FROM db_iemr.m_user WHERE UserID = :userID", userID); + } + + public String currentEmployeeId(Integer userID) { + return single("SELECT EmployeeID FROM db_iemr.m_user WHERE UserID = :userID", userID); + } + + private String single(String sql, Integer userID) { + Query query = entityManager.createNativeQuery(sql); + query.setParameter("userID", userID); + List rows = query.getResultList(); + return rows.isEmpty() ? null : (String) rows.get(0); + } + + public long renameUserRow(Integer userID, String newUserName, String newEmployeeId, + boolean updateContactFields) { + String sql = selectUserUpdate(newUserName, newEmployeeId, updateContactFields); + if (sql == null) { + return 0; + } + + Query query = entityManager.createNativeQuery(sql); + query.setParameter("userID", userID); + if (newUserName != null) { + query.setParameter("newUserName", newUserName); + } + if (newEmployeeId != null) { + query.setParameter("newEmployeeId", newEmployeeId); + } + return query.executeUpdate(); + } + + private static String selectUserUpdate(String newUserName, String newEmployeeId, boolean updateContactFields) { + if (newUserName == null) { + return newEmployeeId == null ? null : SET_EMPLOYEE_ID; + } + if (newEmployeeId == null) { + return updateContactFields ? SET_USER_NAME_AND_CONTACTS : SET_USER_NAME; + } + return updateContactFields ? SET_USER_NAME_CONTACTS_AND_EMPLOYEE_ID : SET_USER_NAME_AND_EMPLOYEE_ID; + } + + public boolean userNameTaken(String userName, Integer excludeUserID) { + return countMatching(COUNT_BY_USER_NAME, userName, excludeUserID) > 0; + } + + public boolean employeeIdTaken(String employeeId, Integer excludeUserID) { + return countMatching(COUNT_BY_EMPLOYEE_ID, employeeId, excludeUserID) > 0; + } + + private long countMatching(String sql, String value, Integer excludeUserID) { + Query query = entityManager.createNativeQuery(sql); + query.setParameter("value", value); + query.setParameter("excludeUserID", excludeUserID); + return toLong(query.getSingleResult()); + } + + private long toLong(Object result) { + if (result == null) { + return 0L; + } + if (result instanceof BigInteger bigInteger) { + return bigInteger.longValue(); + } + return ((Number) result).longValue(); + } +} diff --git a/src/main/java/com/iemr/admin/service/username/UsernameRenameService.java b/src/main/java/com/iemr/admin/service/username/UsernameRenameService.java new file mode 100644 index 0000000..84d8b78 --- /dev/null +++ b/src/main/java/com/iemr/admin/service/username/UsernameRenameService.java @@ -0,0 +1,29 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.service.username; + +import com.iemr.admin.model.username.UsernameRenameRequest; +import com.iemr.admin.model.username.UsernameRenameResponse; + +public interface UsernameRenameService { + UsernameRenameResponse rename(UsernameRenameRequest request) throws Exception; +} diff --git a/src/main/java/com/iemr/admin/service/username/UsernameRenameServiceImpl.java b/src/main/java/com/iemr/admin/service/username/UsernameRenameServiceImpl.java new file mode 100644 index 0000000..e22c905 --- /dev/null +++ b/src/main/java/com/iemr/admin/service/username/UsernameRenameServiceImpl.java @@ -0,0 +1,158 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* 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.admin.service.username; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.iemr.admin.model.username.UsernameRenameRequest; +import com.iemr.admin.model.username.UsernameRenameResponse; +import com.iemr.admin.repository.username.UsernameAuditTables; +import com.iemr.admin.repository.username.UsernameAuditTables.AuditTable; +import com.iemr.admin.repository.username.UsernameRenameRepository; + +@Service +public class UsernameRenameServiceImpl implements UsernameRenameService { + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + private static final int MAX_USERNAME_LENGTH = 20; + private static final int MAX_EMPLOYEE_ID_LENGTH = 20; + + private static final int MAX_CONTACT_LENGTH = 12; + + @Autowired + private UsernameRenameRepository usernameRenameRepository; + + @Override + @Transactional(rollbackFor = Exception.class) + public UsernameRenameResponse rename(UsernameRenameRequest request) throws Exception { + validate(request); + + String oldUserName = request.getOldUserName(); + String newUserName = request.getNewUserName(); + String newEmployeeId = request.getNewEmployeeId(); + logger.info("Username rename starting: userID {}, userNameChanging {}, employeeIdChanging {}", + request.getUserID(), newUserName != null, newEmployeeId != null); + + UsernameRenameResponse response = newResponse(request); + response.setOldEmployeeId(usernameRenameRepository.currentEmployeeId(request.getUserID())); + + long rowsUpdated = usernameRenameRepository.renameUserRow(request.getUserID(), newUserName, newEmployeeId, + request.isUpdateContactFields()); + + if (newUserName != null) { + for (AuditTable table : UsernameAuditTables.TABLES) { + rowsUpdated += usernameRenameRepository.renameInTable(table, oldUserName, newUserName); + } + } + + logger.info("Username rename complete: {} rows updated", rowsUpdated); + return response; + } + + private UsernameRenameResponse newResponse(UsernameRenameRequest request) { + UsernameRenameResponse response = new UsernameRenameResponse(); + response.setOldUserName(request.getOldUserName()); + response.setNewUserName(request.getNewUserName()); + response.setNewEmployeeId(request.getNewEmployeeId()); + response.setUserNameUpdated(request.getNewUserName() != null); + response.setEmployeeIdUpdated(request.getNewEmployeeId() != null); + return response; + } + + private void validate(UsernameRenameRequest request) throws Exception { + if (request == null) { + throw new IllegalArgumentException("Request body is required"); + } + + if (request.getUserID() == null) { + throw new IllegalArgumentException("User ID is required"); + } + + String storedUserName = usernameRenameRepository.currentUserName(request.getUserID()); + if (storedUserName == null) { + throw new IllegalArgumentException("No user found with ID " + request.getUserID()); + } + + String oldUserName = trimToNull(request.getOldUserName()); + if (oldUserName != null && !oldUserName.equals(storedUserName)) { + throw new IllegalArgumentException("User " + request.getUserID() + " is now named " + storedUserName + + ", not " + oldUserName + ". Reload the user list and try again."); + } + request.setOldUserName(storedUserName); + oldUserName = storedUserName; + + request.setNewUserName(resolveNewUserName(request, oldUserName)); + request.setNewEmployeeId(resolveNewEmployeeId(request)); + + if (request.getNewUserName() == null && request.getNewEmployeeId() == null) { + throw new IllegalArgumentException("Nothing to update — enter a new username or a new employee ID"); + } + } + + private String resolveNewUserName(UsernameRenameRequest request, String oldUserName) throws Exception { + String newUserName = trimToNull(request.getNewUserName()); + if (newUserName == null || newUserName.equals(oldUserName)) { + return null; + } + if (newUserName.length() > MAX_USERNAME_LENGTH) { + throw new IllegalArgumentException( + "New username exceeds " + MAX_USERNAME_LENGTH + " characters (m_user.UserName limit)"); + } + if (request.isUpdateContactFields() && newUserName.length() > MAX_CONTACT_LENGTH) { + throw new IllegalArgumentException("New username exceeds " + MAX_CONTACT_LENGTH + + " characters and cannot be written to ContactNo. Either shorten it or " + + "turn off updating contact numbers."); + } + if (usernameRenameRepository.userNameTaken(newUserName, request.getUserID())) { + throw new IllegalArgumentException("Username " + newUserName + " is already in use"); + } + return newUserName; + } + + private String resolveNewEmployeeId(UsernameRenameRequest request) throws Exception { + String newEmployeeId = trimToNull(request.getNewEmployeeId()); + if (newEmployeeId == null + || newEmployeeId.equals(usernameRenameRepository.currentEmployeeId(request.getUserID()))) { + return null; + } + if (newEmployeeId.length() > MAX_EMPLOYEE_ID_LENGTH) { + throw new IllegalArgumentException( + "New employee ID exceeds " + MAX_EMPLOYEE_ID_LENGTH + " characters (m_user.EmployeeID limit)"); + } + if (usernameRenameRepository.employeeIdTaken(newEmployeeId, request.getUserID())) { + throw new IllegalArgumentException("Employee ID " + newEmployeeId + " is already in use"); + } + return newEmployeeId; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +} From b45a485492bbe9fab2c7b506c5f314cae735d7f9 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 8 Sep 2026 14:52:41 +0530 Subject: [PATCH 8/8] test: add unit test cases --- pom.xml | 11 + .../blocking/BlockingControllerTest.java | 577 ++++++++ .../BulkRegistrationControllerTest.java | 185 +++ .../CalibrationControllerTest.java | 177 +++ .../CareStreamCreateOrderControllerTest.java | 66 + .../drugstrength/DrugStrengthTest.java | 157 +++ .../drugtype/DrugtypeControllerTest.java | 160 +++ .../EmailConfigControllerTest.java | 147 ++ .../AshaSupervisorMappingControllerTest.java | 356 +++++ .../EmployeeMasterAgentControllerTest.java | 155 +++ .../employeemaster/EmployeeMasterFixture.java | 117 ++ .../EmployeeMasterLookupControllerTest.java | 555 ++++++++ .../EmployeeMasterWriteControllerTest.java | 760 +++++++++++ .../EmployeeSignatureControllerTest.java | 238 ++++ .../FacilitytypeControllerTest.java | 314 +++++ .../FoetalMonitorControllerTest.java | 355 +++++ .../health/HealthControllerTest.java | 102 ++ .../controller/item/ItemControllerTest.java | 545 ++++++++ .../MItemFacilityMappingControllerTest.java | 259 ++++ .../labmodule/LabModuleControllerTest.java | 356 +++++ .../SmartDiagnosticsControllerTest.java | 103 ++ .../LocationMasterControllerTest.java | 425 ++++++ .../ManufacturerControllerTest.java | 169 +++ .../NodalConfigControllerTest.java | 147 ++ .../ParkingPlaceControllerTest.java | 260 ++++ ...arkingPlaceTalukMappingControllerTest.java | 261 ++++ ...PharmacologicalCategoryControllerTest.java | 169 +++ ...ProviderOnBoardCallTypeControllerTest.java | 288 ++++ ...ProviderOnBoardCategoryControllerTest.java | 688 ++++++++++ .../ProviderOnBoardDrugControllerTest.java | 304 +++++ .../ProviderOnBoardFixture.java | 131 ++ ...roviderOnBoardInstituteControllerTest.java | 557 ++++++++ ...ProviderOnBoardProviderControllerTest.java | 379 ++++++ .../QuestionnaireControllerTest.java | 181 +++ .../rolemaster/RoleMasterControllerTest.java | 464 +++++++ .../ServicePointControllerTest.java | 274 ++++ .../SnomedMappingControllerTest.java | 195 +++ .../stockEntry/StockEntryControllerTest.java | 145 ++ .../stockExit/StockExitControllerTest.java | 99 ++ .../controller/store/StoreControllerTest.java | 453 +++++++ .../SupplierMasterControllerTest.java | 169 +++ .../TeleMedicineControllerTest.java | 205 +++ .../VideoConsultationControllerTest.java | 199 +++ .../controller/uom/UomControllerTest.java | 169 +++ .../uptsu/FacilityControllerTest.java | 155 +++ .../UserParkingPlaceMapControllerTest.java | 289 ++++ .../vanMaster/VanMasterControllerTest.java | 281 ++++ .../VanServicePointMappingControllerTest.java | 206 +++ .../VanSpokeMappingControllerTest.java | 159 +++ .../version/VersionControllerTest.java | 60 + .../VillageMasterControllerTest.java | 177 +++ .../zonemaster/ZoneMasterControllerTest.java | 287 ++++ .../com/iemr/admin/data/BeanContract.java | 245 ++++ .../com/iemr/admin/data/ClassScanner.java | 64 + .../admin/data/DataCarrierContractTest.java | 71 + .../InstituteEmailConfigMapperTest.java | 260 ++++ .../ParkingPlaceTalukMappingMapperTest.java | 134 ++ .../service/apiman/ApimanServiceImplTest.java | 261 ++++ .../service/blocking/BlockingServiceTest.java | 309 +++++ .../BulkRegistrationServiceImplTest.java | 588 ++++++++ .../EmployeeXmlServiceTest.java | 58 + .../CalibrationServiceImplTest.java | 222 +++ .../drugstrangth/DrugStrangthServiceTest.java | 112 ++ .../drugtype/DrugtypeServiceImplTest.java | 113 ++ .../EmailConfigServiceImplTest.java | 189 +++ .../EmployeeMasterServiceImplTest.java | 1195 +++++++++++++++++ .../EmployeeMasterSupportServicesTest.java | 556 ++++++++ .../M_facilitytypeServiceImplTest.java | 196 +++ .../FoetalMonitorServiceImplTest.java | 420 ++++++ .../service/health/HealthServiceTest.java | 472 +++++++ .../service/item/ItemServiceImplTest.java | 284 ++++ .../M_itemfacilitymappingImplTest.java | 195 +++ .../LocationMasterServiceImplTest.java | 180 +++ .../ManufacturerServiceImplTest.java | 113 ++ .../NodalConfigServiceImplTest.java | 190 +++ .../ParkingPlaceServiceImplTest.java | 184 +++ ...rkingPlaceTalukMappingServiceImplTest.java | 151 +++ ...harmacologicalcategoryServiceImplTest.java | 113 ++ .../ProviderOnBoardServicesTest.java | 801 +++++++++++ .../QuestionnaireServiceImplTest.java | 233 ++++ .../rolemaster/RoleMasterServiceImplTest.java | 346 +++++ .../ServicePointServiceImplTest.java | 197 +++ .../snomedMapping/SnomedServiceImplTest.java | 273 ++++ .../stockEntry/StockEntryServiceImplTest.java | 318 +++++ .../stockExit/StockExitServiceImplTest.java | 190 +++ .../service/store/StoreServiceImplTest.java | 664 +++++++++ .../supplier/SupplierServiceImplTest.java | 113 ++ .../telemedicine/TMServiceImplTest.java | 172 +++ .../VideoConsultationAPIServiceImplTest.java | 196 +++ .../VideoConsultationServiceImplTest.java | 269 ++++ .../admin/service/uom/UomServiceImplTest.java | 113 ++ .../uptsu/FacilityServiceImplTest.java | 290 ++++ .../service/user/EncryptUserPasswordTest.java | 171 +++ .../service/user/IemrUserServiceImplTest.java | 283 ++++ .../UserParkingPlaceMapServiceImplTest.java | 233 ++++ .../vanMaster/VanMasterServiceImplTest.java | 215 +++ ...VanServicePointMappingServiceImplTest.java | 170 +++ .../VanSpokeMappingServiceImplTest.java | 201 +++ .../VillageMasterServiceImplTest.java | 150 +++ .../zonemaster/ZoneMasterServiceImplTest.java | 181 +++ .../labmodule/LabModuleServicesTest.java | 613 +++++++++ .../com/iemr/admin/utils/CookieUtilTest.java | 115 ++ .../iemr/admin/utils/FilterConfigTest.java | 84 ++ .../utils/JwtAuthenticationUtilTest.java | 221 +++ .../utils/JwtUserIdValidationFilterTest.java | 415 ++++++ .../com/iemr/admin/utils/JwtUtilTest.java | 198 +++ .../admin/utils/RestTemplateUtilTest.java | 219 +++ .../iemr/admin/utils/TokenDenylistTest.java | 175 +++ .../admin/utils/UserAgentContextTest.java | 88 ++ .../utils/config/ConfigPropertiesTest.java | 169 +++ .../email/GenericEmailServiceImplTest.java | 156 +++ ...AuthorizationHeaderRequestWrapperTest.java | 128 ++ .../http/HTTPRequestInterceptorTest.java | 312 +++++ .../iemr/admin/utils/http/HttpUtilsTest.java | 243 ++++ .../admin/utils/mapper/GsonMappersTest.java | 133 ++ .../admin/utils/mapper/InputMapperTest.java | 133 ++ .../redis/RedisSessionExceptionTest.java | 74 + .../admin/utils/redis/RedisStorageTest.java | 189 +++ .../utils/response/OutputResponseTest.java | 299 +++++ .../sessionobject/SessionObjectTest.java | 143 ++ 120 files changed, 30401 insertions(+) create mode 100644 src/test/java/com/iemr/admin/controller/blocking/BlockingControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/bulkRegistration/BulkRegistrationControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/calibration/CalibrationControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/createorder/CareStreamCreateOrderControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/drugstrength/DrugStrengthTest.java create mode 100644 src/test/java/com/iemr/admin/controller/drugtype/DrugtypeControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/emailconfig/EmailConfigControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/employeemaster/AshaSupervisorMappingControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterAgentControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterFixture.java create mode 100644 src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterLookupControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterWriteControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/employeemaster/EmployeeSignatureControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/facilitytype/FacilitytypeControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/foetalmonitormaster/FoetalMonitorControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/health/HealthControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/item/ItemControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/labmodule/LabModuleControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/labmodule/SmartDiagnosticsControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/locationmaster/LocationMasterControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/manufacturer/ManufacturerControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/nodalConfig/NodalConfigControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/parkingPlace/ParkingPlaceControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/parkingPlace/ParkingPlaceTalukMappingControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/pharmacologicalcategory/PharmacologicalCategoryControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardCallTypeControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardCategoryControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardDrugControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardFixture.java create mode 100644 src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardInstituteControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardProviderControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/questionnaire/QuestionnaireControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/rolemaster/RoleMasterControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/servicePoint/ServicePointControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/snomedMapping/SnomedMappingControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/stockEntry/StockEntryControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/stockExit/StockExitControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/store/StoreControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/supplier/SupplierMasterControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/telemedicine/TeleMedicineControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/telemedicine/VideoConsultationControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/uom/UomControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/uptsu/FacilityControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/userParkingPlaceMap/UserParkingPlaceMapControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/vanMaster/VanMasterControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/vanServicePointMapping/VanServicePointMappingControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/vanSpokeMapping/VanSpokeMappingControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/version/VersionControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/villageMaster/VillageMasterControllerTest.java create mode 100644 src/test/java/com/iemr/admin/controller/zonemaster/ZoneMasterControllerTest.java create mode 100644 src/test/java/com/iemr/admin/data/BeanContract.java create mode 100644 src/test/java/com/iemr/admin/data/ClassScanner.java create mode 100644 src/test/java/com/iemr/admin/data/DataCarrierContractTest.java create mode 100644 src/test/java/com/iemr/admin/mapper/emailconfig/InstituteEmailConfigMapperTest.java create mode 100644 src/test/java/com/iemr/admin/mapper/parkingplacetalukmapping/ParkingPlaceTalukMappingMapperTest.java create mode 100644 src/test/java/com/iemr/admin/service/apiman/ApimanServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/blocking/BlockingServiceTest.java create mode 100644 src/test/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/bulkRegistration/EmployeeXmlServiceTest.java create mode 100644 src/test/java/com/iemr/admin/service/calibration/CalibrationServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/drugstrangth/DrugStrangthServiceTest.java create mode 100644 src/test/java/com/iemr/admin/service/drugtype/DrugtypeServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/emailconfig/EmailConfigServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/employeemaster/EmployeeMasterSupportServicesTest.java create mode 100644 src/test/java/com/iemr/admin/service/facilitytype/M_facilitytypeServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/foetalmonitormaster/FoetalMonitorServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/health/HealthServiceTest.java create mode 100644 src/test/java/com/iemr/admin/service/item/ItemServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/itemfacilitymapping/M_itemfacilitymappingImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/locationmaster/LocationMasterServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/manufacturer/ManufacturerServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/nodalemailconfig/NodalConfigServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/parkingPlace/ParkingPlaceServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/parkingPlace/ParkingPlaceTalukMappingServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/pharmacologicalcategory/PharmacologicalcategoryServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/provideronboard/ProviderOnBoardServicesTest.java create mode 100644 src/test/java/com/iemr/admin/service/questionnaire/QuestionnaireServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/rolemaster/RoleMasterServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/servicePoint/ServicePointServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/snomedMapping/SnomedServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/stockEntry/StockEntryServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/stockExit/StockExitServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/store/StoreServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/supplier/SupplierServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/telemedicine/TMServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/telemedicine/VideoConsultationAPIServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/telemedicine/VideoConsultationServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/uom/UomServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/uptsu/FacilityServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/user/EncryptUserPasswordTest.java create mode 100644 src/test/java/com/iemr/admin/service/user/IemrUserServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/userParkingPlaceMap/UserParkingPlaceMapServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/vanMaster/VanMasterServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/vanServicePointMapping/VanServicePointMappingServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/vanSpokeMapping/VanSpokeMappingServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/villageMaster/VillageMasterServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/service/zonemaster/ZoneMasterServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/sevice/labmodule/LabModuleServicesTest.java create mode 100644 src/test/java/com/iemr/admin/utils/CookieUtilTest.java create mode 100644 src/test/java/com/iemr/admin/utils/FilterConfigTest.java create mode 100644 src/test/java/com/iemr/admin/utils/JwtAuthenticationUtilTest.java create mode 100644 src/test/java/com/iemr/admin/utils/JwtUserIdValidationFilterTest.java create mode 100644 src/test/java/com/iemr/admin/utils/JwtUtilTest.java create mode 100644 src/test/java/com/iemr/admin/utils/RestTemplateUtilTest.java create mode 100644 src/test/java/com/iemr/admin/utils/TokenDenylistTest.java create mode 100644 src/test/java/com/iemr/admin/utils/UserAgentContextTest.java create mode 100644 src/test/java/com/iemr/admin/utils/config/ConfigPropertiesTest.java create mode 100644 src/test/java/com/iemr/admin/utils/gateway/email/GenericEmailServiceImplTest.java create mode 100644 src/test/java/com/iemr/admin/utils/http/AuthorizationHeaderRequestWrapperTest.java create mode 100644 src/test/java/com/iemr/admin/utils/http/HTTPRequestInterceptorTest.java create mode 100644 src/test/java/com/iemr/admin/utils/http/HttpUtilsTest.java create mode 100644 src/test/java/com/iemr/admin/utils/mapper/GsonMappersTest.java create mode 100644 src/test/java/com/iemr/admin/utils/mapper/InputMapperTest.java create mode 100644 src/test/java/com/iemr/admin/utils/redis/RedisSessionExceptionTest.java create mode 100644 src/test/java/com/iemr/admin/utils/redis/RedisStorageTest.java create mode 100644 src/test/java/com/iemr/admin/utils/response/OutputResponseTest.java create mode 100644 src/test/java/com/iemr/admin/utils/sessionobject/SessionObjectTest.java diff --git a/pom.xml b/pom.xml index 062df58..4bc3e13 100644 --- a/pom.xml +++ b/pom.xml @@ -280,6 +280,17 @@ h2 runtime + + org.springframework.boot + spring-boot-starter-test + test + + + jakarta.xml.bind + jakarta.xml.bind-api + + + diff --git a/src/test/java/com/iemr/admin/controller/blocking/BlockingControllerTest.java b/src/test/java/com/iemr/admin/controller/blocking/BlockingControllerTest.java new file mode 100644 index 0000000..b93dab4 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/blocking/BlockingControllerTest.java @@ -0,0 +1,577 @@ +/* +* 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.admin.controller.blocking; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.blocking.M_Providerservicemapping_Blocking; +import com.iemr.admin.data.blocking.M_Serviceprovider_Blocking; +import com.iemr.admin.data.blocking.M_Status1; +import com.iemr.admin.data.blocking.T_Providerservicemappingdetail; +import com.iemr.admin.data.blocking.T_Serviceproviderdetail; +import com.iemr.admin.data.blocking.T_Userdetail; +import com.iemr.admin.data.blocking.UserForBlocking; +import com.iemr.admin.data.blocking.V_Showproviderservicemapping; +import com.iemr.admin.service.blocking.BlockingInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The blocking endpoints suspend and reinstate a provider - whole, per service + * line, per state, or one user at a time - and write an audit row for each + * change of status. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("BlockingController Test Suite") +class BlockingControllerTest { + + private static final Integer PROVIDER_ID = 77; + private static final Integer SERVICE_ID = 3; + private static final Integer STATE_ID = 29; + + @Mock + private BlockingInter blockingInter; + + @InjectMocks + private BlockingController controller; + + private static M_Providerservicemapping_Blocking mapping(Integer mapId) { + M_Providerservicemapping_Blocking mapping = new M_Providerservicemapping_Blocking(); + mapping.setProviderServiceMapID(mapId); + mapping.setServiceProviderID(PROVIDER_ID); + mapping.setServiceID(SERVICE_ID); + mapping.setStateID(STATE_ID); + mapping.setStatusID(1); + return mapping; + } + + private static V_Showproviderservicemapping view(Integer mapId) { + V_Showproviderservicemapping view = new V_Showproviderservicemapping(); + view.setProviderServiceMapID(mapId); + view.setServiceProviderID(PROVIDER_ID); + return view; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertCodeException(String response) { + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(response), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("blockProvider1 should record the previous status before the provider is blocked") + void blockProvider1_shouldRecordPreviousStatus() { + M_Serviceprovider_Blocking stored = new M_Serviceprovider_Blocking(); + stored.setServiceProviderID(PROVIDER_ID); + stored.setServiceProviderName("Piramal Swasthya"); + stored.setStatusID(1); + T_Serviceproviderdetail saved = new T_Serviceproviderdetail(); + saved.setServiceProviderDetailID(9001); + when(blockingInter.getProviderDetailsById(PROVIDER_ID)).thenReturn(stored); + when(blockingInter.saveData(any())).thenReturn(saved); + + String response = controller.blockProvider1( + "{\"serviceProviderID\":77,\"statusID\":2,\"reason\":\"contract ended\"}"); + + assertSuccessContaining(response, "9001"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(T_Serviceproviderdetail.class); + verify(blockingInter).saveData(captor.capture()); + assertEquals(1, captor.getValue().getPreviousStatusID()); + assertEquals(2, captor.getValue().getUpdatedStatusID()); + assertEquals("contract ended", captor.getValue().getReason()); + assertEquals(2, stored.getStatusID(), "the provider must carry the new status when it is saved"); + verify(blockingInter).blockServiceProvider(stored); + } + + @Test + @DisplayName("blockProvider1 should answer an error envelope for a provider that does not exist") + void blockProvider1_shouldAnswerErrorEnvelopeForUnknownProvider() { + when(blockingInter.getProviderDetailsById(PROVIDER_ID)).thenReturn(null); + + assertCodeException(controller.blockProvider1("{\"serviceProviderID\":77,\"statusID\":2}")); + } + + @Test + @DisplayName("blockProvider should write one audit row per service mapping it blocks") + void blockProvider_shouldWriteOneAuditRowPerMapping() { + when(blockingInter.getProviderStatus(PROVIDER_ID)) + .thenReturn(new ArrayList<>(List.of(mapping(4001), mapping(4002)))); + T_Providerservicemappingdetail saved = new T_Providerservicemappingdetail(); + saved.setProviderServiceMapID(4001); + when(blockingInter.savetpsmd(anyList())).thenReturn(new ArrayList<>(List.of(saved))); + + String response = controller.blockProvider( + "{\"serviceProviderID\":77,\"statusID\":2,\"reason\":\"contract ended\"}"); + + assertSuccessContaining(response, "4001"); + verify(blockingInter).blockProvider(PROVIDER_ID, 2); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(blockingInter).savetpsmd(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertEquals("contract ended", captor.getValue().get(0).getReason()); + } + + @Test + @DisplayName("blockProvider should answer an error envelope when the mappings cannot be read") + void blockProvider_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getProviderStatus(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.blockProvider("{\"serviceProviderID\":77,\"statusID\":2}")); + } + + @Test + @DisplayName("blockProviderByServiceId should block only the service line the caller names") + void blockProviderByServiceId_shouldBlockOnlyNamedServiceLine() { + when(blockingInter.getProviderStatusByProviderAndServiceId(PROVIDER_ID, SERVICE_ID)) + .thenReturn(new ArrayList<>(List.of(mapping(4001)))); + T_Providerservicemappingdetail saved = new T_Providerservicemappingdetail(); + saved.setProviderServiceMapID(4001); + when(blockingInter.savetpsmd(anyList())).thenReturn(new ArrayList<>(List.of(saved))); + + assertSuccessContaining(controller.blockProviderByServiceId( + "{\"serviceProviderID\":77,\"serviceID\":3,\"statusID\":2}"), "4001"); + verify(blockingInter).blockProviderByProviderIdAndServiceId(PROVIDER_ID, SERVICE_ID, 2); + } + + @Test + @DisplayName("blockProviderByServiceId should answer an error envelope when the mappings cannot be read") + void blockProviderByServiceId_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getProviderStatusByProviderAndServiceId(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure( + controller.blockProviderByServiceId("{\"serviceProviderID\":77,\"serviceID\":3,\"statusID\":2}")); + } + + @Test + @DisplayName("getProviderStatus should answer the mapping view for the provider") + void getProviderStatus_shouldAnswerMappingView() { + when(blockingInter.getProviderStatus1(PROVIDER_ID)).thenReturn(new ArrayList<>(List.of(view(4001)))); + + assertSuccessContaining(controller.getProviderStatus("{\"serviceProviderID\":77}"), "4001"); + } + + @Test + @DisplayName("getProviderStatus should answer an error envelope when the view cannot be read") + void getProviderStatus_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getProviderStatus1(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getProviderStatus("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getProviderStatus1 should answer the second mapping view for the provider") + void getProviderStatus1_shouldAnswerSecondView() { + when(blockingInter.getProviderStatus2(PROVIDER_ID)).thenReturn(new ArrayList<>(List.of(view(4001)))); + + assertSuccessContaining(controller.getProviderStatus1("{\"serviceProviderID\":77}"), "4001"); + } + + @Test + @DisplayName("getProviderStatus1 should answer an error envelope when the view cannot be read") + void getProviderStatus1_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getProviderStatus2(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getProviderStatus1("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getServiceLiensUsingProvider should answer the service lines the provider runs") + void getServiceLiensUsingProvider_shouldAnswerServiceLines() { + when(blockingInter.getServiceLiensUsingProvider(PROVIDER_ID)) + .thenReturn(new ArrayList<>(List.of(mapping(4001)))); + + assertSuccessContaining(controller.getServiceLiensUsingProvider("{\"serviceProviderID\":77}"), "4001"); + } + + @Test + @DisplayName("getServiceLiensUsingProvider should answer an error envelope when the lookup fails") + void getServiceLiensUsingProvider_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getServiceLiensUsingProvider(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getServiceLiensUsingProvider("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getProviderStatusByProviderAndServiceId should narrow the view to the service line") + void getProviderStatusByProviderAndServiceId_shouldNarrowToServiceLine() { + when(blockingInter.getProviderStatusByProviderAndServiceId2(PROVIDER_ID, SERVICE_ID)) + .thenReturn(new ArrayList<>(List.of(view(4001)))); + + assertSuccessContaining(controller.getProviderStatusByProviderAndServiceId( + "{\"serviceProviderID\":77,\"serviceID\":3}"), "4001"); + } + + @Test + @DisplayName("getProviderStatusByProviderAndServiceId should answer an error envelope when the lookup fails") + void getProviderStatusByProviderAndServiceId_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getProviderStatusByProviderAndServiceId2(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller + .getProviderStatusByProviderAndServiceId("{\"serviceProviderID\":77,\"serviceID\":3}")); + } + + @Test + @DisplayName("blockProviderByService should record the previous status of the one mapping it blocks") + void blockProviderByService_shouldRecordPreviousStatus() { + M_Providerservicemapping_Blocking stored = mapping(4001); + T_Providerservicemappingdetail saved = new T_Providerservicemappingdetail(); + saved.setProviderServiceMapID(4001); + when(blockingInter.getProviderServiceMappingDetails(PROVIDER_ID, STATE_ID, SERVICE_ID)).thenReturn(stored); + when(blockingInter.savetpsdData(any())).thenReturn(saved); + + assertSuccessContaining(controller.blockProviderByService("{\"serviceProviderID\":77,\"stateID\":29," + + "\"serviceID\":3,\"statusID\":2,\"reason\":\"suspended\"}"), "4001"); + verify(blockingInter).blockProviderByService(PROVIDER_ID, STATE_ID, SERVICE_ID, 2); + } + + @Test + @DisplayName("blockProviderByService should answer an error envelope for a mapping that does not exist") + void blockProviderByService_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(blockingInter.getProviderServiceMappingDetails(any(), any(), any())).thenReturn(null); + + assertCodeException(controller.blockProviderByService( + "{\"serviceProviderID\":77,\"stateID\":29,\"serviceID\":3,\"statusID\":2}")); + } + + @Test + @DisplayName("getProviderStatusByService should answer the view for the service line in the state") + void getProviderStatusByService_shouldAnswerViewForServiceInState() { + when(blockingInter.getProviderServiceMappingDetails2(PROVIDER_ID, STATE_ID, SERVICE_ID)) + .thenReturn(new ArrayList<>(List.of(view(4001)))); + + assertSuccessContaining(controller.getProviderStatusByService( + "{\"serviceProviderID\":77,\"stateID\":29,\"serviceID\":3}"), "4001"); + } + + @Test + @DisplayName("getProviderStatusByService should answer an error envelope when the lookup fails") + void getProviderStatusByService_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getProviderServiceMappingDetails2(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getProviderStatusByService("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("blockProviderByState should write one audit row per mapping in the state") + void blockProviderByState_shouldWriteOneAuditRowPerMapping() { + when(blockingInter.getProviderStateMappingDetails(PROVIDER_ID, STATE_ID)) + .thenReturn(List.of(mapping(4001), mapping(4002))); + T_Providerservicemappingdetail saved = new T_Providerservicemappingdetail(); + saved.setProviderServiceMapID(4001); + when(blockingInter.savetpsmd(anyList())).thenReturn(new ArrayList<>(List.of(saved))); + + assertSuccessContaining(controller.blockProviderByState( + "{\"serviceProviderID\":77,\"stateID\":29,\"statusID\":2,\"reason\":\"suspended\"}"), "4001"); + verify(blockingInter).blockProviderByState(PROVIDER_ID, STATE_ID, 2); + } + + @Test + @DisplayName("blockProviderByState should answer an error envelope when the mappings cannot be read") + void blockProviderByState_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getProviderStateMappingDetails(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure( + controller.blockProviderByState("{\"serviceProviderID\":77,\"stateID\":29,\"statusID\":2}")); + } + + @Test + @DisplayName("getProviderStatusByState should answer the view for the state") + void getProviderStatusByState_shouldAnswerViewForState() { + when(blockingInter.getProviderStateMappingDetails1(PROVIDER_ID, STATE_ID)) + .thenReturn(new ArrayList<>(List.of(view(4001)))); + + assertSuccessContaining( + controller.getProviderStatusByState("{\"serviceProviderID\":77,\"stateID\":29}"), "4001"); + } + + @Test + @DisplayName("getProviderStatusByState should answer an error envelope when the lookup fails") + void getProviderStatusByState_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getProviderStateMappingDetails1(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getProviderStatusByState("{\"serviceProviderID\":77,\"stateID\":29}")); + } + + @Test + @DisplayName("blockUser should record the user's previous credentials before the block takes effect") + void blockUser_shouldRecordPreviousCredentials() { + UserForBlocking stored = new UserForBlocking(); + stored.setUserID(3117); + stored.setUserName("asha.rao"); + stored.setPassword("old-secret"); + stored.setStatusID(1); + when(blockingInter.getUserDetailByUserId(3117)).thenReturn(stored); + + String response = controller.blockUser("{\"userID\":3117,\"statusID\":2," + + "\"updatedPassword\":\"new-secret\",\"updatedStatusID\":2}"); + + assertSuccessContaining(response, "jai"); + verify(blockingInter).blockUser(3117, 2); + + ArgumentCaptor captor = ArgumentCaptor.forClass(T_Userdetail.class); + verify(blockingInter).saveUserDetails(captor.capture()); + assertEquals("old-secret", captor.getValue().getPreviousPassword()); + assertEquals("new-secret", captor.getValue().getUpdatedPassword()); + assertEquals(1, captor.getValue().getPreviousStatusID()); + } + + @Test + @DisplayName("blockUser should answer an error envelope for a user that does not exist") + void blockUser_shouldAnswerErrorEnvelopeForUnknownUser() { + when(blockingInter.getUserDetailByUserId(3117)).thenReturn(null); + + assertCodeException(controller.blockUser("{\"userID\":3117,\"statusID\":2}")); + } + + @Test + @DisplayName("getStatus should answer every status on record") + void getStatus_shouldAnswerEveryStatus() { + M_Status1 status = new M_Status1(); + status.setStatusID(1); + status.setStatus("Active"); + when(blockingInter.getStatusData()).thenReturn(new ArrayList<>(List.of(status))); + + assertSuccessContaining(controller.getStatus("{}"), "Active"); + } + + @Test + @DisplayName("getStatus should answer an error envelope when the lookup fails") + void getStatus_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getStatusData()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getStatus("{}")); + } + + @Test + @DisplayName("ProviderStateAndServiceLines should create one mapping per state the request names") + void providerStateAndServiceLines_shouldCreateOneMappingPerState() { + when(blockingInter.AddServiceProvider(anyList())) + .thenReturn(new ArrayList<>(List.of(mapping(4001)))); + + String response = controller.ProviderStateAndServiceLines("[{\"serviceProviderID\":77,\"serviceID\":3," + + "\"createdBy\":\"admin\",\"statusID\":1,\"stateID1\":[29,30]}]"); + + assertSuccessContaining(response, "4001"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(blockingInter).AddServiceProvider(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertEquals(29, captor.getValue().get(0).getStateID()); + } + + @Test + @DisplayName("ProviderStateAndServiceLines should fall back to the state on the record when none is listed") + void providerStateAndServiceLines_shouldFallBackToRecordState() { + when(blockingInter.AddServiceProvider(anyList())).thenReturn(new ArrayList<>()); + + controller.ProviderStateAndServiceLines("[{\"serviceProviderID\":77,\"serviceID\":3," + + "\"stateID\":29,\"createdBy\":\"admin\",\"statusID\":1,\"stateID1\":[]}]"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(blockingInter).AddServiceProvider(captor.capture()); + assertEquals(1, captor.getValue().size()); + assertEquals(29, captor.getValue().get(0).getStateID()); + } + + @Test + @DisplayName("ProviderStateAndServiceLines should answer an error envelope when no states are named") + void providerStateAndServiceLines_shouldAnswerErrorEnvelopeWithoutStates() { + assertCodeException(controller.ProviderStateAndServiceLines("[{\"serviceProviderID\":77}]")); + } + + @Test + @DisplayName("deleteProviderStateAndServiceLines should build one record per service line named") + void deleteProviderStateAndServiceLines_shouldBuildOneRecordPerService() { + when(blockingInter.AddServiceProvider(anyList())).thenReturn(new ArrayList<>(List.of(mapping(4001)))); + + String response = controller.deleteProviderStateAndServiceLines("{\"serviceProviderID\":77," + + "\"stateID\":29,\"createdBy\":\"admin\",\"statusID\":1,\"serviceID1\":[3,4]}"); + + assertSuccessContaining(response, "4001"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(blockingInter).AddServiceProvider(captor.capture()); + assertEquals(2, captor.getValue().size()); + } + + @Test + @DisplayName("deleteProviderStateAndServiceLines should answer an error envelope when no services are named") + void deleteProviderStateAndServiceLines_shouldAnswerErrorEnvelopeWithoutServices() { + assertCodeException(controller.deleteProviderStateAndServiceLines("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("createCitMappingwithServiceLines should answer the summary the service reports") + void createCitMappingwithServiceLines_shouldAnswerSummary() { + when(blockingInter.mapctidata(anyList())).thenReturn("2 campaigns mapped"); + + assertSuccessContaining(controller.createCitMappingwithServiceLines( + "[{\"providerServiceMapID\":4001,\"cTI_CampaignName\":\"104\"}]"), "2 campaigns mapped"); + } + + @Test + @DisplayName("createCitMappingwithServiceLines should answer an error envelope when the mapping fails") + void createCitMappingwithServiceLines_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.mapctidata(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createCitMappingwithServiceLines("[{\"providerServiceMapID\":4001}]")); + } + + @Test + @DisplayName("getMappedServiceLinesAndStatetoProvider should answer the mappings the service resolves") + void getMappedServiceLines_shouldAnswerResolvedMappings() { + when(blockingInter.getServiceLiensUsingProvider1(any())) + .thenReturn(new ArrayList<>(List.of(mapping(4001)))); + + assertSuccessContaining( + controller.getMappedServiceLinesAndStatetoProvider("{\"serviceProviderID\":77}"), "4001"); + } + + @Test + @DisplayName("getMappedServiceLinesAndStatetoProvider should answer an error envelope when the lookup fails") + void getMappedServiceLines_shouldAnswerErrorEnvelopeOnFailure() { + when(blockingInter.getServiceLiensUsingProvider1(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getMappedServiceLinesAndStatetoProvider("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("mapProviderAndServiceLines should mark every new mapping active") + void mapProviderAndServiceLines_shouldMarkNewMappingsActive() { + when(blockingInter.AddServiceProvider(anyList())).thenReturn(new ArrayList<>(List.of(mapping(4001)))); + + String response = controller.mapProviderAndServiceLines("[{\"serviceProviderID\":77,\"serviceID\":3," + + "\"createdBy\":\"admin\",\"stateID1\":[29,30]}]"); + + assertSuccessContaining(response, "4001"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(blockingInter).AddServiceProvider(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertEquals(1, captor.getValue().get(0).getStatusID(), "a new mapping starts active"); + } + + @Test + @DisplayName("mapProviderAndServiceLines should fall back to the state on the record when none is listed") + void mapProviderAndServiceLines_shouldFallBackToRecordState() { + when(blockingInter.AddServiceProvider(anyList())).thenReturn(new ArrayList<>()); + + controller.mapProviderAndServiceLines("[{\"serviceProviderID\":77,\"serviceID\":3," + + "\"stateID\":29,\"createdBy\":\"admin\",\"stateID1\":[]}]"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(blockingInter).AddServiceProvider(captor.capture()); + assertEquals(1, captor.getValue().size()); + } + + @Test + @DisplayName("mapProviderAndServiceLines should answer an error envelope when no states are named") + void mapProviderAndServiceLines_shouldAnswerErrorEnvelopeWithoutStates() { + assertCodeException(controller.mapProviderAndServiceLines("[{\"serviceProviderID\":77}]")); + } + + @Test + @DisplayName("editMappedServiceLinesAndStatetoProvider should copy the edits onto the stored mapping") + void editMappedServiceLines_shouldCopyEdits() { + M_Providerservicemapping_Blocking stored = mapping(4001); + when(blockingInter.getDataByProviderServiceMapId(4001)).thenReturn(stored); + when(blockingInter.updateProviderData(stored)).thenReturn(stored); + + String response = controller.editMappedServiceLinesAndStatetoProvider("{\"providerServiceMapID\":4001," + + "\"serviceProviderID\":78,\"serviceID\":4,\"stateID\":30,\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "4001"); + assertEquals(78, stored.getServiceProviderID()); + assertEquals(30, stored.getStateID()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("editMappedServiceLinesAndStatetoProvider should answer an error envelope for an unknown mapping") + void editMappedServiceLines_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(blockingInter.getDataByProviderServiceMapId(4001)).thenReturn(null); + + assertCodeException( + controller.editMappedServiceLinesAndStatetoProvider("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("deleteMappedServiceLinesAndStatetoProvider should mark the mapping deleted") + void deleteMappedServiceLines_shouldMarkMappingDeleted() { + M_Providerservicemapping_Blocking stored = mapping(4001); + when(blockingInter.getDataByProviderServiceMapId(4001)).thenReturn(stored); + when(blockingInter.updateProviderData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteMappedServiceLinesAndStatetoProvider( + "{\"providerServiceMapID\":4001,\"deleted\":true}"), "4001"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteMappedServiceLinesAndStatetoProvider should answer an error envelope for an unknown mapping") + void deleteMappedServiceLines_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(blockingInter.getDataByProviderServiceMapId(4001)).thenReturn(null); + + assertCodeException(controller.deleteMappedServiceLinesAndStatetoProvider( + "{\"providerServiceMapID\":4001,\"deleted\":true}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/bulkRegistration/BulkRegistrationControllerTest.java b/src/test/java/com/iemr/admin/controller/bulkRegistration/BulkRegistrationControllerTest.java new file mode 100644 index 0000000..ab12766 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/bulkRegistration/BulkRegistrationControllerTest.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.admin.controller.bulkRegistration; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +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 org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; + +import com.iemr.admin.data.bulkuser.BulkRegistrationError; +import com.iemr.admin.data.employeemaster.M_User1; +import com.iemr.admin.repo.employeemaster.EmployeeMasterRepoo; +import com.iemr.admin.service.bulkRegistration.BulkRegistrationService; +import com.iemr.admin.service.bulkRegistration.BulkRegistrationServiceImpl; +import com.iemr.admin.service.bulkRegistration.EmployeeXmlService; +import com.iemr.admin.service.locationmaster.LocationMasterServiceInter; + +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.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +/** + * The bulk registration endpoint reports how much of an uploaded sheet was + * accepted, and hands the rejected rows back as a spreadsheet the uploader can + * correct and resubmit. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("BulkRegistrationController Test Suite") +class BulkRegistrationControllerTest { + + @Mock + private EmployeeXmlService employeeXmlService; + + @Spy + private BulkRegistrationServiceImpl bulkRegistrationServiceimpl; + + @Mock + private BulkRegistrationService bulkRegistrationService; + + @Mock + private EmployeeMasterRepoo employeeMasterRepoo; + + @Mock + private LocationMasterServiceInter locationMasterServiceInter; + + @InjectMocks + private BulkRegistrationController controller; + + @Test + @DisplayName("registerBulkUser should report how many rows were accepted and what was rejected") + void registerBulkUser_shouldReportAcceptedAndRejectedRows() throws Exception { + doAnswer(call -> { + bulkRegistrationServiceimpl.totalEmployeeListSize = 3; + bulkRegistrationServiceimpl.m_bulkUser.add(new M_User1()); + bulkRegistrationServiceimpl.m_bulkUser.add(new M_User1()); + bulkRegistrationServiceimpl.errorLogs.add("Row 3: Title is missing."); + return null; + }).when(bulkRegistrationService).registerBulkUser(anyString(), anyString(), anyString(), anyInt()); + + ResponseEntity> response = controller.registerBulkUser( + "", "auth", "admin", new MockHttpServletRequest(), 77); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("Success", response.getBody().get("status")); + assertEquals(200, response.getBody().get("statusCode")); + assertEquals(3, response.getBody().get("totalUser")); + assertEquals(2, response.getBody().get("registeredUser")); + assertTrue(response.getBody().get("error").toString().contains("Title is missing.")); + } + + @Test + @DisplayName("registerBulkUser should clear the accumulated state so the next upload starts clean") + void registerBulkUser_shouldClearAccumulatedState() throws Exception { + doAnswer(call -> { + bulkRegistrationServiceimpl.totalEmployeeListSize = 1; + bulkRegistrationServiceimpl.m_bulkUser.add(new M_User1()); + bulkRegistrationServiceimpl.errorLogs.add("Row 1: Title is missing."); + return null; + }).when(bulkRegistrationService).registerBulkUser(anyString(), anyString(), anyString(), anyInt()); + + controller.registerBulkUser("", "auth", "admin", new MockHttpServletRequest(), 77); + + assertTrue(bulkRegistrationServiceimpl.m_bulkUser.isEmpty()); + assertTrue(bulkRegistrationServiceimpl.errorLogs.isEmpty()); + assertEquals(0, bulkRegistrationServiceimpl.totalEmployeeListSize); + } + + @Test + @DisplayName("registerBulkUser should discard the rejected rows of a previous upload before it starts") + void registerBulkUser_shouldDiscardPreviousRejectedRows() throws Exception { + bulkRegistrationServiceimpl.bulkRegistrationErrors.add(new BulkRegistrationError()); + + controller.registerBulkUser("", "auth", "admin", new MockHttpServletRequest(), 77); + + verify(bulkRegistrationService).registerBulkUser("", "auth", "admin", 77); + } + + @Test + @DisplayName("registerBulkUser should report the failure rather than a partial success") + void registerBulkUser_shouldReportFailure() throws Exception { + doThrow(new IllegalStateException("the upload could not be read")) + .when(bulkRegistrationService).registerBulkUser(anyString(), anyString(), anyString(), anyInt()); + + ResponseEntity> response = controller.registerBulkUser( + "", "auth", "admin", new MockHttpServletRequest(), 77); + + assertEquals(500, response.getBody().get("statusCode")); + assertEquals("the upload could not be read", response.getBody().get("message")); + } + + @Test + @DisplayName("downloadErrorSheet should answer the rejected rows as a spreadsheet attachment") + void downloadErrorSheet_shouldAnswerSpreadsheetAttachment() { + BulkRegistrationError error = new BulkRegistrationError(); + error.setUserName("EMP-1"); + error.setError(java.util.List.of("Title is missing.")); + bulkRegistrationServiceimpl.bulkRegistrationErrors.add(error); + + ResponseEntity response = controller.downloadErrorSheet(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().length > 0); + assertTrue(response.getHeaders().getFirst("Content-Disposition").contains("error_log.xlsx")); + assertTrue(bulkRegistrationServiceimpl.bulkRegistrationErrors.isEmpty(), + "the rejected rows are handed over once, then cleared"); + } + + @Test + @DisplayName("downloadErrorSheet should still answer a sheet when nothing was rejected") + void downloadErrorSheet_shouldAnswerSheetWithoutRejectedRows() { + ResponseEntity response = controller.downloadErrorSheet(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.getBody().length > 0); + } + + @Test + @DisplayName("downloadErrorSheet should answer a failure rather than an empty file when the sheet cannot be built") + void downloadErrorSheet_shouldAnswerFailureWhenSheetCannotBeBuilt() { + doThrow(new IllegalStateException("out of memory")).when(bulkRegistrationServiceimpl).insertErrorLog(); + + ResponseEntity response = controller.downloadErrorSheet(); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertNull(response.getBody()); + } +} diff --git a/src/test/java/com/iemr/admin/controller/calibration/CalibrationControllerTest.java b/src/test/java/com/iemr/admin/controller/calibration/CalibrationControllerTest.java new file mode 100644 index 0000000..1f3a60e --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/calibration/CalibrationControllerTest.java @@ -0,0 +1,177 @@ +/* +* 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.admin.controller.calibration; + +import org.json.JSONObject; +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.admin.data.calibration.CalibrationStrip; +import com.iemr.admin.service.calibration.CalibrationService; +import com.iemr.admin.utils.exception.IEMRException; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The calibration screen keeps the test strip codes a provider calibrates + * against. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("CalibrationController Test Suite") +class CalibrationControllerTest { + + private static final String REQUEST = "{\"calibrationStripID\":6601,\"stripCode\":\"STRIP-77\"," + + "\"providerServiceMapID\":4001,\"deleted\":false,\"createdBy\":\"admin\"}"; + + @Mock + private CalibrationService calibrationService; + + @InjectMocks + private CalibrationController controller; + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static String errorMessageOf(String response) { + return new JSONObject(response).getString("errorMessage"); + } + + @Test + @DisplayName("createCalibrationStrip should confirm the strip it recorded") + void create_shouldConfirmRecordedStrip() throws Exception { + when(calibrationService.saveData(any(CalibrationStrip.class))).thenReturn(1); + + String response = controller.createCalibrationStrip(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Data saved successfully"), response); + } + + @Test + @DisplayName("createCalibrationStrip should report the failure when no strip was recorded") + void create_shouldReportNothingRecorded() throws Exception { + when(calibrationService.saveData(any(CalibrationStrip.class))).thenReturn(0); + + assertEquals("Error while saving Calibration master data", + errorMessageOf(controller.createCalibrationStrip(REQUEST))); + } + + @Test + @DisplayName("createCalibrationStrip should pass on the refusal in the service's own words") + void create_shouldPassOnRefusal() throws Exception { + when(calibrationService.saveData(any(CalibrationStrip.class))) + .thenThrow(new IEMRException("Strip code already exists")); + + assertEquals("Strip code already exists", errorMessageOf(controller.createCalibrationStrip(REQUEST))); + } + + @Test + @DisplayName("fetchCalibrationStrips should answer the strips the provider holds") + void fetch_shouldAnswerHeldStrips() throws Exception { + when(calibrationService.fetchData(any(CalibrationStrip.class))) + .thenReturn("{\"calibrationData\":[{\"stripCode\":\"STRIP-77\"}]}"); + + String response = controller.fetchCalibrationStrips(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("STRIP-77"), response); + } + + @Test + @DisplayName("fetchCalibrationStrips should report the failure when the strips cannot be answered") + void fetch_shouldReportLookupFailure() throws Exception { + when(calibrationService.fetchData(any(CalibrationStrip.class))) + .thenThrow(new IEMRException("Error while fetching Calibration data")); + + assertEquals(OutputResponse.USERID_FAILURE, statusCodeOf(controller.fetchCalibrationStrips(REQUEST))); + } + + @Test + @DisplayName("deleteCalibrationStrip should confirm the retirement it recorded") + void delete_shouldConfirmRetirement() throws Exception { + when(calibrationService.deleteData(any(CalibrationStrip.class))).thenReturn(1); + + String response = controller.deleteCalibrationStrip(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Data deleted successfully"), response); + } + + @Test + @DisplayName("deleteCalibrationStrip should report the failure when no strip was retired") + void delete_shouldReportNothingRetired() throws Exception { + when(calibrationService.deleteData(any(CalibrationStrip.class))).thenReturn(0); + + assertEquals("Error while updating Calibration master data", + errorMessageOf(controller.deleteCalibrationStrip(REQUEST))); + } + + @Test + @DisplayName("deleteCalibrationStrip should report the failure when the request is refused") + void delete_shouldReportRefusal() throws Exception { + when(calibrationService.deleteData(any(CalibrationStrip.class))) + .thenThrow(new IEMRException("Invalid request")); + + assertEquals(OutputResponse.USERID_FAILURE, statusCodeOf(controller.deleteCalibrationStrip(REQUEST))); + } + + @Test + @DisplayName("updateCalibrationStrip should confirm the change it recorded") + void update_shouldConfirmRecordedChange() throws Exception { + when(calibrationService.updateData(any(CalibrationStrip.class))).thenReturn(1); + + String response = controller.updateCalibrationStrip(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Data updated successfully"), response); + } + + @Test + @DisplayName("updateCalibrationStrip should report the failure when no strip was changed") + void update_shouldReportNothingChanged() throws Exception { + when(calibrationService.updateData(any(CalibrationStrip.class))).thenReturn(0); + + assertEquals("Error while updating Calibration master data", + errorMessageOf(controller.updateCalibrationStrip(REQUEST))); + } + + @Test + @DisplayName("updateCalibrationStrip should pass on the refusal in the service's own words") + void update_shouldPassOnRefusal() throws Exception { + when(calibrationService.updateData(any(CalibrationStrip.class))) + .thenThrow(new IEMRException("Error while updating data")); + + assertEquals("Error while updating data", errorMessageOf(controller.updateCalibrationStrip(REQUEST))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/createorder/CareStreamCreateOrderControllerTest.java b/src/test/java/com/iemr/admin/controller/createorder/CareStreamCreateOrderControllerTest.java new file mode 100644 index 0000000..8c209fc --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/createorder/CareStreamCreateOrderControllerTest.java @@ -0,0 +1,66 @@ +/* +* 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.admin.controller.createorder; + +import org.json.JSONObject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.admin.utils.response.OutputResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The imaging order screens talk to a radiology server over a hard-coded socket + * address, so only the request reading that happens before the connection is + * opened can be checked here; a request the screen cannot read is refused + * without any connection being attempted at all. + */ +@DisplayName("CareStreamCreateOrderController Test Suite") +class CareStreamCreateOrderControllerTest { + + private static final String UNREADABLE_REQUEST = "{not json"; + + private final CareStreamCreateOrderController controller = new CareStreamCreateOrderController(); + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + @Test + @DisplayName("createOrder should refuse a request it cannot read rather than reach the radiology server") + void createOrder_shouldRefuseUnreadableRequest() throws Exception { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.createOrder(UNREADABLE_REQUEST))); + } + + @Test + @DisplayName("UpdateOrder should refuse a request it cannot read rather than reach the radiology server") + void updateOrder_shouldRefuseUnreadableRequest() throws Exception { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.UpdateOrder(UNREADABLE_REQUEST))); + } + + @Test + @DisplayName("deleteOrder should refuse a request it cannot read rather than reach the radiology server") + void deleteOrder_shouldRefuseUnreadableRequest() throws Exception { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.deleteOrder(UNREADABLE_REQUEST))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/drugstrength/DrugStrengthTest.java b/src/test/java/com/iemr/admin/controller/drugstrength/DrugStrengthTest.java new file mode 100644 index 0000000..def2c57 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/drugstrength/DrugStrengthTest.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.admin.controller.drugstrength; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.drugstrangth.M_104DrugStrength; +import com.iemr.admin.service.drugstrangth.DrugStrangthInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The drug strength screen keeps the strengths a drug can be dispensed in. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("DrugStrength controller Test Suite") +class DrugStrengthTest { + + private static final Integer STRENGTH_ID = 33; + + @Mock + private DrugStrangthInter durgStrangthInter; + + @InjectMocks + private DrugStrength controller; + + private static M_104DrugStrength strength() { + M_104DrugStrength strength = new M_104DrugStrength(); + strength.setDrugStrengthID(STRENGTH_ID); + strength.setDrugStrength("500 mg"); + strength.setDrugStrengthDesc("Standard adult dose"); + return strength; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + @Test + @DisplayName("createDrugStrangth should answer the strengths it added") + void create_shouldAnswerAddedStrengths() { + when(durgStrangthInter.createDrugStrangth(anyList())).thenReturn(new ArrayList<>(List.of(strength()))); + + assertSuccessContaining(controller.createDrugStrangth("[{\"drugStrength\":\"500 mg\"}]"), "500 mg"); + } + + @Test + @DisplayName("createDrugStrangth should report the failure when the strength cannot be added") + void create_shouldReportStorageFailure() { + when(durgStrangthInter.createDrugStrangth(anyList())).thenThrow(new RuntimeException("already on file")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.createDrugStrangth("[{\"drugStrength\":\"500 mg\"}]"))); + } + + @Test + @DisplayName("getDrugStrangth should answer the strengths on file") + void get_shouldAnswerStrengthsOnFile() { + when(durgStrangthInter.getDrugStrangth()).thenReturn(new ArrayList<>(List.of(strength()))); + + assertSuccessContaining(controller.getDrugStrangth("{}"), "500 mg"); + } + + @Test + @DisplayName("getDrugStrangth should report the failure when the list cannot be answered") + void get_shouldReportLookupFailure() { + when(durgStrangthInter.getDrugStrangth()).thenThrow(new RuntimeException("connection reset")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getDrugStrangth("{}"))); + } + + @Test + @DisplayName("updateDrugStrangth should answer the strength whose details it changed") + void update_shouldAnswerChangedStrength() { + M_104DrugStrength stored = strength(); + when(durgStrangthInter.updateDrugStrangth(STRENGTH_ID)).thenReturn(stored); + when(durgStrangthInter.saveupdatedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.updateDrugStrangth( + "{\"drugStrengthID\":33,\"drugStrength\":\"650 mg\",\"drugStrengthDesc\":\"Higher dose\"," + + "\"modifiedBy\":\"admin\"}"), + "650 mg"); + assertEquals("Higher dose", stored.getDrugStrengthDesc()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("updateDrugStrangth should report the failure when the strength is unknown") + void update_shouldReportUnknownStrength() { + when(durgStrangthInter.updateDrugStrangth(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, + statusCodeOf(controller.updateDrugStrangth("{\"drugStrengthID\":-1}"))); + } + + @Test + @DisplayName("deleteDrugStrangth should answer the strength it retired") + void delete_shouldAnswerRetiredStrength() { + M_104DrugStrength stored = strength(); + when(durgStrangthInter.updateDrugStrangth(STRENGTH_ID)).thenReturn(stored); + when(durgStrangthInter.saveupdatedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteDrugStrangth("{\"drugStrengthID\":33,\"deleted\":true}"), "33"); + assertEquals(Boolean.TRUE, stored.getDeleted()); + } + + @Test + @DisplayName("deleteDrugStrangth should report the failure when the strength is unknown") + void delete_shouldReportUnknownStrength() { + when(durgStrangthInter.updateDrugStrangth(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, + statusCodeOf(controller.deleteDrugStrangth("{\"drugStrengthID\":-1,\"deleted\":true}"))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/drugtype/DrugtypeControllerTest.java b/src/test/java/com/iemr/admin/controller/drugtype/DrugtypeControllerTest.java new file mode 100644 index 0000000..91d9d01 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/drugtype/DrugtypeControllerTest.java @@ -0,0 +1,160 @@ +/* +* 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.admin.controller.drugtype; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.drugtype.M_Drugtype; +import com.iemr.admin.service.drugtype.DrugtypeInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The drug type screen keeps the dosage forms a provider stocks. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("DrugtypeController Test Suite") +class DrugtypeControllerTest { + + private static final Integer DRUG_TYPE_ID = 21; + + @Mock + private DrugtypeInter drugtypeInter; + + @InjectMocks + private DrugtypeController controller; + + private static M_Drugtype drugType() { + M_Drugtype drugType = new M_Drugtype(); + drugType.setDrugTypeID(DRUG_TYPE_ID); + drugType.setDrugTypeName("Tablet"); + drugType.setDrugTypeCode("TAB"); + drugType.setProviderServiceMapID(4001); + return drugType; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + @Test + @DisplayName("createManufacturer should answer the drug types it added") + void create_shouldAnswerAddedDrugTypes() { + when(drugtypeInter.createDrugtypeData(anyList())).thenReturn(new ArrayList<>(List.of(drugType()))); + + assertSuccessContaining( + controller.createManufacturer("[{\"drugTypeName\":\"Tablet\",\"drugTypeCode\":\"TAB\"}]"), "Tablet"); + } + + @Test + @DisplayName("createManufacturer should report the failure when the drug type cannot be added") + void create_shouldReportStorageFailure() { + when(drugtypeInter.createDrugtypeData(anyList())).thenThrow(new RuntimeException("already on file")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.createManufacturer("[{\"drugTypeName\":\"Tablet\"}]"))); + } + + @Test + @DisplayName("getManufacturer should answer the drug types the provider stocks") + void get_shouldAnswerStockedDrugTypes() { + when(drugtypeInter.getDrugtypeData(4001)).thenReturn(new ArrayList<>(List.of(drugType()))); + + assertSuccessContaining(controller.getManufacturer("{\"providerServiceMapID\":4001}"), "Tablet"); + } + + @Test + @DisplayName("getManufacturer should report the failure when the list cannot be answered") + void get_shouldReportLookupFailure() { + when(drugtypeInter.getDrugtypeData(any())).thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getManufacturer("{\"providerServiceMapID\":4001}"))); + } + + @Test + @DisplayName("editManufacturer should answer the drug type whose details it changed") + void edit_shouldAnswerChangedDrugType() { + M_Drugtype stored = drugType(); + when(drugtypeInter.editDrugtypeData(DRUG_TYPE_ID)).thenReturn(stored); + when(drugtypeInter.saveeditDrugtype(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editManufacturer( + "{\"drugTypeID\":21,\"drugTypeName\":\"Capsule\",\"drugTypeCode\":\"CAP\",\"status\":\"A\"," + + "\"modifiedBy\":\"admin\"}"), + "Capsule"); + assertEquals("CAP", stored.getDrugTypeCode()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("editManufacturer should report the failure when the drug type is unknown") + void edit_shouldReportUnknownDrugType() { + when(drugtypeInter.editDrugtypeData(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, + statusCodeOf(controller.editManufacturer("{\"drugTypeID\":-1}"))); + } + + @Test + @DisplayName("deleteManufacturer should answer the drug type it retired") + void delete_shouldAnswerRetiredDrugType() { + M_Drugtype stored = drugType(); + when(drugtypeInter.editDrugtypeData(DRUG_TYPE_ID)).thenReturn(stored); + when(drugtypeInter.saveeditDrugtype(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteManufacturer("{\"drugTypeID\":21,\"deleted\":true}"), "21"); + assertEquals(Boolean.TRUE, stored.getDeleted()); + } + + @Test + @DisplayName("deleteManufacturer should report the failure when the drug type is unknown") + void delete_shouldReportUnknownDrugType() { + when(drugtypeInter.editDrugtypeData(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, + statusCodeOf(controller.deleteManufacturer("{\"drugTypeID\":-1,\"deleted\":true}"))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/emailconfig/EmailConfigControllerTest.java b/src/test/java/com/iemr/admin/controller/emailconfig/EmailConfigControllerTest.java new file mode 100644 index 0000000..de89ef1 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/emailconfig/EmailConfigControllerTest.java @@ -0,0 +1,147 @@ +/* +* 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.admin.controller.emailconfig; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.mock.web.MockHttpServletRequest; + +import com.iemr.admin.model.emailconfig.AuthEmailRequest; +import com.iemr.admin.model.emailconfig.AuthEmailResponse; +import com.iemr.admin.model.emailconfig.CreateAuthEmailRequestModel; +import com.iemr.admin.model.emailconfig.UpdateAuthEmailRequest; +import com.iemr.admin.service.emailconfig.EmailConfigService; +import com.iemr.admin.utils.mapper.OutputMapper; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyList; +import static org.mockito.Mockito.when; + +/** + * The email config screen keeps the authority mailboxes a complaint is copied + * to. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("EmailConfigController Test Suite") +class EmailConfigControllerTest { + + @Mock + private EmailConfigService emailConfigService; + + @InjectMocks + private EmailConfigController controller; + + private final MockHttpServletRequest servletRequest = new MockHttpServletRequest(); + + @BeforeEach + @DisplayName("Prime the shared output builder the screens publish through") + void setUp() { + new OutputMapper(); + } + + private static AuthEmailResponse mailbox() { + AuthEmailResponse mailbox = new AuthEmailResponse(); + mailbox.setEmailID("dho.bengaluru@example.org"); + return mailbox; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + @Test + @DisplayName("saveConfig should answer the mailboxes it recorded") + void save_shouldAnswerRecordedMailboxes() { + when(emailConfigService.saveEmailConfigs(anyList())).thenReturn(List.of(mailbox())); + + String response = controller.saveConfig(List.of(new CreateAuthEmailRequestModel()), servletRequest); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("dho.bengaluru@example.org"), response); + } + + @Test + @DisplayName("saveConfig should report the failure when the mailbox cannot be recorded") + void save_shouldReportStorageFailure() { + when(emailConfigService.saveEmailConfigs(anyList())).thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.saveConfig(new ArrayList<>(), servletRequest))); + } + + @Test + @DisplayName("getEmailConfigs should answer the mailboxes matching what the caller narrowed by") + void get_shouldAnswerMatchingMailboxes() { + when(emailConfigService.getAllEmailConfigs(any(AuthEmailRequest.class))).thenReturn(List.of(mailbox())); + + String response = controller.getEmailConfigs(new AuthEmailRequest(), servletRequest); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("dho.bengaluru@example.org"), response); + } + + @Test + @DisplayName("getEmailConfigs should report the failure when the mailboxes cannot be answered") + void get_shouldReportLookupFailure() { + when(emailConfigService.getAllEmailConfigs(any(AuthEmailRequest.class))) + .thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getEmailConfigs(new AuthEmailRequest(), servletRequest))); + } + + @Test + @DisplayName("updateEmailConfig should answer the mailbox as it stands after the change") + void update_shouldAnswerChangedMailbox() { + when(emailConfigService.updateEmailConfigs(any(UpdateAuthEmailRequest.class))).thenReturn(mailbox()); + + String response = controller.updateEmailConfig(new UpdateAuthEmailRequest(), servletRequest); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("dho.bengaluru@example.org"), response); + } + + @Test + @DisplayName("updateEmailConfig should report the failure when the change cannot be recorded") + void update_shouldReportStorageFailure() { + when(emailConfigService.updateEmailConfigs(any(UpdateAuthEmailRequest.class))) + .thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.updateEmailConfig(new UpdateAuthEmailRequest(), servletRequest))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/employeemaster/AshaSupervisorMappingControllerTest.java b/src/test/java/com/iemr/admin/controller/employeemaster/AshaSupervisorMappingControllerTest.java new file mode 100644 index 0000000..108c41f --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/employeemaster/AshaSupervisorMappingControllerTest.java @@ -0,0 +1,356 @@ +/* +* 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.admin.controller.employeemaster; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.json.JSONObject; +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.admin.data.employeemaster.AshaSupervisorMapping; +import com.iemr.admin.data.employeemaster.M_UserServiceRoleMapping2; +import com.iemr.admin.data.store.M_Facility; +import com.iemr.admin.repo.employeemaster.EmployeeMasterRepo; +import com.iemr.admin.repository.store.MainStoreRepo; +import com.iemr.admin.service.employeemaster.AshaSupervisorMappingService; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The ASHA supervisor endpoints decide which ASHAs each supervisor oversees at + * each facility, so a wrong mapping puts a health worker under the wrong + * supervisor. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("AshaSupervisorMappingController Test Suite") +class AshaSupervisorMappingControllerTest { + + @Mock + private AshaSupervisorMappingService ashaSupervisorMappingService; + + @Mock + private EmployeeMasterRepo employeeMasterRepo; + + @Mock + private MainStoreRepo mainStoreRepo; + + @InjectMocks + private AshaSupervisorMappingController controller; + + private static AshaSupervisorMapping mapping(Long id, Integer supervisorId, Integer ashaId) { + AshaSupervisorMapping mapping = new AshaSupervisorMapping(); + mapping.setId(id); + mapping.setSupervisorUserID(supervisorId); + mapping.setAshaUserID(ashaId); + return mapping; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + @Test + @DisplayName("getAshasByFacility should answer the ASHAs attached to the facilities the caller lists") + void getAshasByFacility_shouldAnswerAshasForListedFacilities() { + M_UserServiceRoleMapping2 asha = new M_UserServiceRoleMapping2(); + asha.setuSRMappingID(9001); + asha.setUserID(3117); + when(ashaSupervisorMappingService.getAshasByFacility(List.of(501, 502))) + .thenReturn(new ArrayList<>(List.of(asha))); + + assertSuccessContaining(controller.getAshasByFacility("{\"facilityIDs\":[501,502]}"), "9001"); + } + + @Test + @DisplayName("getAshasByFacility should fall back to the single facility id when no list is sent") + void getAshasByFacility_shouldFallBackToSingleFacilityId() { + when(ashaSupervisorMappingService.getAshasByFacility(List.of(501))).thenReturn(new ArrayList<>()); + + controller.getAshasByFacility("{\"facilityID\":501}"); + + verify(ashaSupervisorMappingService).getAshasByFacility(List.of(501)); + } + + @Test + @DisplayName("getAshasByFacility should ask for nothing when the request names no facility at all") + void getAshasByFacility_shouldAskForNothingWithoutAFacility() { + when(ashaSupervisorMappingService.getAshasByFacility(null)).thenReturn(new ArrayList<>()); + + controller.getAshasByFacility("{}"); + + verify(ashaSupervisorMappingService).getAshasByFacility(null); + } + + @Test + @DisplayName("getAshasByFacility should answer an error envelope when the lookup fails") + void getAshasByFacility_shouldAnswerErrorEnvelopeOnFailure() { + when(ashaSupervisorMappingService.getAshasByFacility(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getAshasByFacility("{\"facilityIDs\":[501]}"))); + } + + @Test + @DisplayName("saveAshaSupervisorMapping should answer the mappings the service stored") + void saveAshaSupervisorMapping_shouldAnswerStoredMappings() { + when(ashaSupervisorMappingService.saveAshaSupervisorMappings(anyList())) + .thenReturn(new ArrayList<>(List.of(mapping(1L, 3117, 4001)))); + + assertSuccessContaining( + controller.saveAshaSupervisorMapping("[{\"supervisorUserID\":3117,\"ashaUserID\":4001}]"), "3117"); + } + + @Test + @DisplayName("saveAshaSupervisorMapping should answer an error envelope when the store fails") + void saveAshaSupervisorMapping_shouldAnswerErrorEnvelopeOnFailure() { + when(ashaSupervisorMappingService.saveAshaSupervisorMappings(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.saveAshaSupervisorMapping("[{\"supervisorUserID\":3117}]"))); + } + + @Test + @DisplayName("getSupervisorMappingByFacility should answer the mappings at the facility") + void getSupervisorMappingByFacility_shouldAnswerFacilityMappings() { + when(ashaSupervisorMappingService.getSupervisorMappingByFacility(501)) + .thenReturn(new ArrayList<>(List.of(mapping(1L, 3117, 4001)))); + + assertSuccessContaining(controller.getSupervisorMappingByFacility("{\"facilityID\":501}"), "3117"); + } + + @Test + @DisplayName("getSupervisorMappingByFacility should answer an error envelope when the lookup fails") + void getSupervisorMappingByFacility_shouldAnswerErrorEnvelopeOnFailure() { + when(ashaSupervisorMappingService.getSupervisorMappingByFacility(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getSupervisorMappingByFacility("{\"facilityID\":501}"))); + } + + @Test + @DisplayName("deleteAshaSupervisorMapping should retire the mappings the caller names") + void deleteAshaSupervisorMapping_shouldRetireNamedMappings() { + String response = controller + .deleteAshaSupervisorMapping("{\"supervisorUserID\":3117,\"facilityIDs\":[501,502]}"); + + assertSuccessContaining(response, "Deleted successfully"); + verify(ashaSupervisorMappingService).deleteBySupervisorAndFacilities(3117, List.of(501, 502), "Admin"); + } + + @Test + @DisplayName("deleteAshaSupervisorMapping should refuse a request that names no supervisor") + void deleteAshaSupervisorMapping_shouldRefuseRequestWithoutSupervisor() { + String response = controller.deleteAshaSupervisorMapping("{\"facilityIDs\":[501]}"); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response)); + assertTrue(response.contains("supervisorUserID and facilityIDs are required"), response); + verify(ashaSupervisorMappingService, never()).deleteBySupervisorAndFacilities(anyInt(), anyList(), anyString()); + } + + @Test + @DisplayName("deleteAshaSupervisorMapping should refuse a request that names no facilities") + void deleteAshaSupervisorMapping_shouldRefuseRequestWithoutFacilities() { + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.deleteAshaSupervisorMapping("{\"supervisorUserID\":3117}"))); + } + + @Test + @DisplayName("deleteAshaSupervisorMapping should answer an error envelope when the retirement fails") + void deleteAshaSupervisorMapping_shouldAnswerErrorEnvelopeOnFailure() { + org.mockito.Mockito.doThrow(new IllegalStateException("no connection")) + .when(ashaSupervisorMappingService) + .deleteBySupervisorAndFacilities(anyInt(), anyList(), anyString()); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller + .deleteAshaSupervisorMapping("{\"supervisorUserID\":3117,\"facilityIDs\":[501]}"))); + } + + @Test + @DisplayName("updateAshaSupervisorMappingAtomically should replace the old mappings in one step") + void updateAtomically_shouldReplaceMappingsInOneStep() { + when(ashaSupervisorMappingService.updateAshaMappingsAtomically(anyInt(), anyList(), anyList(), anyString())) + .thenReturn(new ArrayList<>(List.of(mapping(1L, 3117, 4001)))); + + String response = controller.updateAshaSupervisorMappingAtomically("{\"supervisorUserID\":3117," + + "\"modifiedBy\":\"admin\",\"facilityIDs\":[501,502]," + + "\"newMappings\":[{\"supervisorUserID\":3117,\"ashaUserID\":4001,\"facilityID\":501}]}"); + + assertSuccessContaining(response, "3117"); + verify(ashaSupervisorMappingService).updateAshaMappingsAtomically( + org.mockito.ArgumentMatchers.eq(3117), org.mockito.ArgumentMatchers.eq(List.of(501, 502)), + anyList(), org.mockito.ArgumentMatchers.eq("admin")); + } + + @Test + @DisplayName("updateAshaSupervisorMappingAtomically should attribute the change to Admin when none is named") + void updateAtomically_shouldAttributeToAdminByDefault() { + when(ashaSupervisorMappingService.updateAshaMappingsAtomically(anyInt(), anyList(), anyList(), anyString())) + .thenReturn(new ArrayList<>()); + + controller.updateAshaSupervisorMappingAtomically("{\"supervisorUserID\":3117,\"facilityIDs\":[501]}"); + + verify(ashaSupervisorMappingService).updateAshaMappingsAtomically( + org.mockito.ArgumentMatchers.eq(3117), anyList(), anyList(), + org.mockito.ArgumentMatchers.eq("Admin")); + } + + @Test + @DisplayName("updateAshaSupervisorMappingAtomically should answer an error envelope without a supervisor") + void updateAtomically_shouldAnswerErrorEnvelopeWithoutSupervisor() { + assertEquals(OutputResponse.CODE_EXCEPTION, + statusCodeOf(controller.updateAshaSupervisorMappingAtomically("{\"facilityIDs\":[501]}"))); + } + + @Test + @DisplayName("restoreAshaSupervisorMapping should reinstate the mappings the caller names") + void restoreAshaSupervisorMapping_shouldReinstateNamedMappings() { + String response = controller.restoreAshaSupervisorMapping("{\"ids\":[1,2]}"); + + assertSuccessContaining(response, "Restored successfully"); + verify(ashaSupervisorMappingService).restoreMappings(List.of(1L, 2L), "Admin"); + } + + @Test + @DisplayName("restoreAshaSupervisorMapping should refuse a request that names no mappings") + void restoreAshaSupervisorMapping_shouldRefuseRequestWithoutIds() { + String response = controller.restoreAshaSupervisorMapping("{}"); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response)); + assertTrue(response.contains("ids are required"), response); + } + + @Test + @DisplayName("restoreAshaSupervisorMapping should answer an error envelope when the reinstatement fails") + void restoreAshaSupervisorMapping_shouldAnswerErrorEnvelopeOnFailure() { + org.mockito.Mockito.doThrow(new IllegalStateException("no connection")) + .when(ashaSupervisorMappingService).restoreMappings(anyList(), anyString()); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.restoreAshaSupervisorMapping("{\"ids\":[1]}"))); + } + + @Test + @DisplayName("getFacilityByMappingID should answer the facility the mapping points at") + void getFacilityByMappingID_shouldAnswerMappedFacility() { + M_UserServiceRoleMapping2 mapping = new M_UserServiceRoleMapping2(); + mapping.setuSRMappingID(9001); + mapping.setFacilityID(501); + M_Facility facility = new M_Facility(); + facility.setFacilityID(501); + facility.setFacilityName("PHC North"); + facility.setFacilityTypeID(3); + facility.setRuralUrban("Rural"); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.of(mapping)); + when(mainStoreRepo.findByFacilityIDAndDeleted(501, false)).thenReturn(facility); + + String response = controller.getFacilityByMappingID("{\"uSRMappingID\":9001}"); + + assertSuccessContaining(response, "PHC North"); + assertTrue(response.contains("Rural"), response); + } + + @Test + @DisplayName("getFacilityByMappingID should report a facility that has since been deleted") + void getFacilityByMappingID_shouldReportDeletedFacility() { + M_UserServiceRoleMapping2 mapping = new M_UserServiceRoleMapping2(); + mapping.setuSRMappingID(9001); + mapping.setFacilityID(501); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.of(mapping)); + when(mainStoreRepo.findByFacilityIDAndDeleted(501, false)).thenReturn(null); + + assertSuccessContaining(controller.getFacilityByMappingID("{\"uSRMappingID\":9001}"), "facilityDeleted"); + } + + @Test + @DisplayName("getFacilityByMappingID should answer no facility for a mapping that was retired") + void getFacilityByMappingID_shouldAnswerNoFacilityForRetiredMapping() { + M_UserServiceRoleMapping2 mapping = new M_UserServiceRoleMapping2(); + mapping.setuSRMappingID(9001); + mapping.setFacilityID(501); + mapping.setDeleted(Boolean.TRUE); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.of(mapping)); + + assertSuccessContaining(controller.getFacilityByMappingID("{\"uSRMappingID\":9001}"), "\"data\":{}"); + verify(mainStoreRepo, never()).findByFacilityIDAndDeleted(anyInt(), org.mockito.ArgumentMatchers.anyBoolean()); + } + + @Test + @DisplayName("getFacilityByMappingID should answer no facility for a mapping that names none") + void getFacilityByMappingID_shouldAnswerNoFacilityWhenMappingNamesNone() { + M_UserServiceRoleMapping2 mapping = new M_UserServiceRoleMapping2(); + mapping.setuSRMappingID(9001); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.of(mapping)); + + assertSuccessContaining(controller.getFacilityByMappingID("{\"uSRMappingID\":9001}"), "\"data\":{}"); + } + + @Test + @DisplayName("getFacilityByMappingID should answer no facility for a mapping that does not exist") + void getFacilityByMappingID_shouldAnswerNoFacilityForUnknownMapping() { + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.empty()); + + assertSuccessContaining(controller.getFacilityByMappingID("{\"uSRMappingID\":9001}"), "\"data\":{}"); + } + + @Test + @DisplayName("getFacilityByMappingID should answer no facility when the request names no mapping") + void getFacilityByMappingID_shouldAnswerNoFacilityWithoutMappingId() { + assertSuccessContaining(controller.getFacilityByMappingID("{}"), "\"data\":{}"); + verify(employeeMasterRepo, never()).findById(anyInt()); + } + + @Test + @DisplayName("getFacilityByMappingID should answer an error envelope when the lookup fails") + void getFacilityByMappingID_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterRepo.findById(9001)).thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getFacilityByMappingID("{\"uSRMappingID\":9001}"))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterAgentControllerTest.java b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterAgentControllerTest.java new file mode 100644 index 0000000..f3e07c2 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterAgentControllerTest.java @@ -0,0 +1,155 @@ +/* +* 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.admin.controller.employeemaster; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.admin.data.employeemaster.USRAgentMapping; +import com.iemr.admin.utils.exception.IEMRException; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The agent endpoints hand out and reclaim the CTI agent ids an employee logs + * into the call centre with. + */ +@DisplayName("EmployeeMasterController agent Test Suite") +class EmployeeMasterAgentControllerTest extends EmployeeMasterFixture { + + private static final String AGENT_REQUEST = "{\"providerServiceMapID\":4001,\"cti_CampaignName\":\"104\"}"; + + private static USRAgentMapping agent(Integer id, String agentId) { + return USRAgentMapping.initializeAllUSRAgentMapping(id, 9001, null, 4001, null, agentId, + "agent-secret", "104", Boolean.TRUE); + } + + @Test + @DisplayName("getAvailableAgentIds should answer the agent ids still free on the campaign") + void getAvailableAgentIds_shouldAnswerFreeAgentIds() throws Exception { + when(usrAgentMappingService.getAvailableAgentIds(AGENT_REQUEST)).thenReturn(List.of(agent(1, "A-1"))); + + assertSuccessContaining(controller.getAvailableAgentIds(AGENT_REQUEST), "A-1"); + } + + @Test + @DisplayName("getAvailableAgentIds should answer an error envelope when the lookup fails") + void getAvailableAgentIds_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(usrAgentMappingService.getAvailableAgentIds(anyString())) + .thenThrow(new IEMRException("providerServiceMapID is required")); + + assertIemrFailure(controller.getAvailableAgentIds("{}")); + } + + @Test + @DisplayName("createUSRAgentMapping should answer the mappings the service stored") + void createUSRAgentMapping_shouldAnswerStoredMappings() throws Exception { + when(usrAgentMappingService.createUSRAgentMapping(anyString())).thenReturn(List.of(agent(1, "A-1"))); + + assertSuccessContaining( + controller.createUSRAgentMapping("[{\"agentID\":\"A-1\",\"providerServiceMapID\":4001}]"), "A-1"); + } + + @Test + @DisplayName("createUSRAgentMapping should answer an error envelope when the store fails") + void createUSRAgentMapping_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(usrAgentMappingService.createUSRAgentMapping(anyString())) + .thenThrow(new IEMRException("agentID is required")); + + assertIemrFailure(controller.createUSRAgentMapping("[{}]")); + } + + @Test + @DisplayName("getAvailableCampaigns should answer the campaigns configured for the mapping") + void getAvailableCampaigns_shouldAnswerConfiguredCampaigns() throws Exception { + when(usrAgentMappingService.getAvailableCampaigns(anyString())).thenReturn(List.of("104", "1097")); + + assertSuccessContaining(controller.getAvailableCampaigns(AGENT_REQUEST), "104"); + } + + @Test + @DisplayName("getAvailableCampaigns should answer an error envelope when the lookup fails") + void getAvailableCampaigns_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(usrAgentMappingService.getAvailableCampaigns(anyString())) + .thenThrow(new IEMRException("providerServiceMapID is required")); + + assertIemrFailure(controller.getAvailableCampaigns("{}")); + } + + @Test + @DisplayName("updateAgentIds should answer how many agent ids the service changed") + void updateAgentIds_shouldAnswerChangedCount() throws Exception { + when(usrAgentMappingService.updateAgentIds(anyString())).thenReturn(1); + + assertSuccessContaining(controller.updateAgentIds("{\"isAvailable\":false,\"usrMappingID\":9001}"), "1"); + verify(usrAgentMappingService).updateAgentIds("{\"isAvailable\":false,\"usrMappingID\":9001}"); + } + + @Test + @DisplayName("updateAgentIds should answer an error envelope when the change fails") + void updateAgentIds_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(usrAgentMappingService.updateAgentIds(anyString())) + .thenThrow(new IEMRException("usrAgentMappingID is required")); + + assertIemrFailure(controller.updateAgentIds("{}")); + } + + @Test + @DisplayName("getAllAgentIds should answer every agent id under the mapping") + void getAllAgentIds_shouldAnswerEveryAgentId() throws Exception { + when(usrAgentMappingService.getAllAgentIds(anyString())).thenReturn(List.of(agent(1, "A-1"))); + + assertSuccessContaining(controller.getAllAgentIds(AGENT_REQUEST), "A-1"); + } + + @Test + @DisplayName("getAllAgentIds should answer an error envelope when the lookup fails") + void getAllAgentIds_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(usrAgentMappingService.getAllAgentIds(anyString())) + .thenThrow(new IEMRException("providerServiceMapID is required")); + + assertIemrFailure(controller.getAllAgentIds("{}")); + } + + @Test + @DisplayName("updateCTICampaignNameMapping should answer how many mappings moved campaign") + void updateCTICampaignNameMapping_shouldAnswerChangedCount() throws Exception { + when(usrAgentMappingService.updateCTICampaignNameMapping(anyString())).thenReturn(1); + + assertSuccessContaining( + controller.updateCTICampaignNameMapping("{\"cti_CampaignName\":\"1097\",\"usrAgentMappingID\":1}"), + "1"); + } + + @Test + @DisplayName("updateCTICampaignNameMapping should answer an error envelope when the change fails") + void updateCTICampaignNameMapping_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(usrAgentMappingService.updateCTICampaignNameMapping(anyString())) + .thenThrow(new IEMRException("usrAgentMappingID is required")); + + assertIemrFailure(controller.updateCTICampaignNameMapping("{}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterFixture.java b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterFixture.java new file mode 100644 index 0000000..f446f3c --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterFixture.java @@ -0,0 +1,117 @@ +/* +* 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.admin.controller.employeemaster; + +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +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 org.springframework.mock.web.MockHttpServletRequest; + +import com.iemr.admin.data.employeemaster.M_User1; +import com.iemr.admin.data.employeemaster.M_UserDemographics; +import com.iemr.admin.data.employeemaster.M_UserServiceRoleMapping2; +import com.iemr.admin.service.employeemaster.EmployeeMasterInter; +import com.iemr.admin.service.employeemaster.M_DesignationInter; +import com.iemr.admin.service.employeemaster.USRAgentMappingService; +import com.iemr.admin.utils.response.OutputResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Shared mocks and helpers for the suites over the employee master controller. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +abstract class EmployeeMasterFixture { + + protected static final String AUTH_HEADER = "sess-0d5f3a7c"; + + @Mock + protected M_DesignationInter m_DesignationInter; + + @Mock + protected EmployeeMasterInter employeeMasterInter; + + @Mock + protected USRAgentMappingService usrAgentMappingService; + + @InjectMocks + protected EmployeeMasterController controller; + + protected MockHttpServletRequest request; + + @BeforeEach + void prepareRequest() { + request = new MockHttpServletRequest(); + request.addHeader("Authorization", AUTH_HEADER); + } + + protected static M_User1 user(Integer id, String userName) { + M_User1 user = new M_User1(); + user.setUserID(id); + user.setUserName(userName); + return user; + } + + protected static M_UserDemographics demographics(Integer userId, String fathersName) { + M_UserDemographics demographics = new M_UserDemographics(); + demographics.setUserID(userId); + demographics.setFathersName(fathersName); + return demographics; + } + + protected static M_UserServiceRoleMapping2 roleMapping(Integer id, Integer userId) { + M_UserServiceRoleMapping2 mapping = new M_UserServiceRoleMapping2(); + mapping.setuSRMappingID(id); + mapping.setUserID(userId); + return mapping; + } + + protected static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + protected static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + protected static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + /** + * Asserts the envelope reports the failure the response builder maps an + * {@link com.iemr.admin.utils.exception.IEMRException} onto. + */ + protected static void assertIemrFailure(String response) { + assertEquals(OutputResponse.USERID_FAILURE, statusCodeOf(response), response); + } + + protected static void assertCodeException(String response) { + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(response), response); + } +} diff --git a/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterLookupControllerTest.java b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterLookupControllerTest.java new file mode 100644 index 0000000..2676ee6 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterLookupControllerTest.java @@ -0,0 +1,555 @@ +/* +* 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.admin.controller.employeemaster; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.admin.data.employeemaster.M_Community; +import com.iemr.admin.data.employeemaster.M_Designation; +import com.iemr.admin.data.employeemaster.M_Gender; +import com.iemr.admin.data.employeemaster.M_ProviderServiceMap1; +import com.iemr.admin.data.employeemaster.M_Religion; +import com.iemr.admin.data.employeemaster.M_Role; +import com.iemr.admin.data.employeemaster.M_Title; +import com.iemr.admin.data.employeemaster.M_User1; +import com.iemr.admin.data.employeemaster.M_UserServiceRoleMapping2; +import com.iemr.admin.data.employeemaster.M_Userqualification; +import com.iemr.admin.data.employeemaster.Showofficedetails1; +import com.iemr.admin.data.employeemaster.Showuserdetailsfromuserservicerolemapping; +import com.iemr.admin.data.employeemaster.V_Showuser; + +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; + +/** + * The read-only endpoints back the employee search screens and the drop-downs + * that feed the employee forms. + */ +@DisplayName("EmployeeMasterController lookup Test Suite") +class EmployeeMasterLookupControllerTest extends EmployeeMasterFixture { + + private static Showuserdetailsfromuserservicerolemapping userDetail(Integer userId, String name) { + Showuserdetailsfromuserservicerolemapping detail = new Showuserdetailsfromuserservicerolemapping(); + detail.setUserID(userId); + detail.setUserName(name); + return detail; + } + + private static V_Showuser showUser(Integer userId, String name) { + V_Showuser user = new V_Showuser(); + user.setUserID(userId); + user.setUserName(name); + return user; + } + + @Test + @DisplayName("getAllRole should answer every role on record") + void getAllRole_shouldAnswerEveryRole() { + M_Role role = new M_Role(); + role.setRoleID(11); + role.setRoleName("Provider Admin"); + when(employeeMasterInter.getAllRole()).thenReturn(new ArrayList<>(List.of(role))); + + assertSuccessContaining(controller.getAllRole("{}"), "Provider Admin"); + } + + @Test + @DisplayName("getAllRole should answer an error envelope when the lookup fails") + void getAllRole_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getAllRole()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllRole("{}")); + } + + @Test + @DisplayName("searchEmployee should answer the role mappings on record") + void searchEmployee_shouldAnswerRoleMappings() { + when(employeeMasterInter.getEmployeeDetails()) + .thenReturn(new ArrayList<>(List.of(roleMapping(9001, 3117)))); + + assertSuccessContaining(controller.searchEmployee("{}"), "9001"); + } + + @Test + @DisplayName("searchEmployee should answer an error envelope when the search fails") + void searchEmployee_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee("{}")); + } + + @Test + @DisplayName("searchEmployee1 should answer the second view of the role mappings") + void searchEmployee1_shouldAnswerRoleMappings() { + when(employeeMasterInter.getEmployeeDetails1()) + .thenReturn(new ArrayList<>(List.of(roleMapping(9001, 3117)))); + + assertSuccessContaining(controller.searchEmployee1("{}"), "9001"); + } + + @Test + @DisplayName("searchEmployee1 should answer an error envelope when the search fails") + void searchEmployee1_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails1()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee1("{}")); + } + + @Test + @DisplayName("searchEmployee2 should narrow the search to the provider and its state") + void searchEmployee2_shouldNarrowByProviderAndState() { + when(employeeMasterInter.getEmployeeDetails2(77, 29)) + .thenReturn(new ArrayList<>(List.of(userDetail(3117, "dr.mehta")))); + + assertSuccessContaining( + controller.searchEmployee2("{\"serviceProviderID\":77,\"pSMStateID\":29}"), "dr.mehta"); + } + + @Test + @DisplayName("searchEmployee2 should answer an error envelope when the search fails") + void searchEmployee2_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails2(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee2("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("searchEmployee3 should narrow the search to the provider and role") + void searchEmployee3_shouldNarrowByProviderAndRole() { + when(employeeMasterInter.getEmployeeDetails3(77, 11)) + .thenReturn(new ArrayList<>(List.of(userDetail(3117, "dr.mehta")))); + + assertSuccessContaining( + controller.searchEmployee3("{\"serviceProviderID\":77,\"roleID\":11}"), "dr.mehta"); + } + + @Test + @DisplayName("searchEmployee3 should answer an error envelope when the search fails") + void searchEmployee3_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails3(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee3("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("searchEmployee4 should narrow the search to the provider") + void searchEmployee4_shouldNarrowByProvider() { + when(employeeMasterInter.getEmployeeDetails4(77)) + .thenReturn(new ArrayList<>(List.of(showUser(3117, "dr.mehta")))); + + assertSuccessContaining(controller.searchEmployee4("{\"serviceProviderID\":77}"), "dr.mehta"); + } + + @Test + @DisplayName("searchEmployee4 should answer an error envelope when the search fails") + void searchEmployee4_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails4(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee4("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("searchEmployee5 should answer every user on record") + void searchEmployee5_shouldAnswerEveryUser() { + when(employeeMasterInter.getEmployeeDetails5()) + .thenReturn(new ArrayList<>(List.of(showUser(3117, "dr.mehta")))); + + assertSuccessContaining(controller.searchEmployee5("{}"), "dr.mehta"); + } + + @Test + @DisplayName("searchEmployee5 should answer an error envelope when the search fails") + void searchEmployee5_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails5()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee5("{}")); + } + + @Test + @DisplayName("searchEmployee6 should narrow the search to one user under the provider") + void searchEmployee6_shouldNarrowByProviderAndUser() { + when(employeeMasterInter.getEmployeeDetails6(77, 3117)) + .thenReturn(new ArrayList<>(List.of(userDetail(3117, "dr.mehta")))); + + assertSuccessContaining( + controller.searchEmployee6("{\"serviceProviderID\":77,\"userID\":3117}"), "dr.mehta"); + } + + @Test + @DisplayName("searchEmployee6 should answer an error envelope when the search fails") + void searchEmployee6_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails6(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee6("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("searchEmployee7 should narrow the search down to the working district") + void searchEmployee7_shouldNarrowByDistrict() { + when(employeeMasterInter.getEmployeeDetails7(77, 29, 301)) + .thenReturn(new ArrayList<>(List.of(userDetail(3117, "dr.mehta")))); + + assertSuccessContaining(controller.searchEmployee7( + "{\"serviceProviderID\":77,\"pSMStateID\":29,\"workingDistrictID\":301}"), "dr.mehta"); + } + + @Test + @DisplayName("searchEmployee7 should answer an error envelope when the search fails") + void searchEmployee7_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails7(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee7("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("searchEmployee8 should narrow the search down to the working location") + void searchEmployee8_shouldNarrowByLocation() { + when(employeeMasterInter.getEmployeeDetails8(77, 29, 301, 401)) + .thenReturn(new ArrayList<>(List.of(userDetail(3117, "dr.mehta")))); + + assertSuccessContaining(controller.searchEmployee8("{\"serviceProviderID\":77,\"pSMStateID\":29," + + "\"workingDistrictID\":301,\"workingLocationID\":401}"), "dr.mehta"); + } + + @Test + @DisplayName("searchEmployee8 should answer an error envelope when the search fails") + void searchEmployee8_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails8(any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee8("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("searchEmployee9 should narrow the search to the role within the state") + void searchEmployee9_shouldNarrowByStateAndRole() { + when(employeeMasterInter.getEmployeeDetails9(77, 29, 11)) + .thenReturn(new ArrayList<>(List.of(userDetail(3117, "dr.mehta")))); + + assertSuccessContaining(controller.searchEmployee9( + "{\"serviceProviderID\":77,\"pSMStateID\":29,\"roleID\":11}"), "dr.mehta"); + } + + @Test + @DisplayName("searchEmployee9 should answer an error envelope when the search fails") + void searchEmployee9_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails9(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee9("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("searchEmployee10 should pass every filter the screen sends through") + void searchEmployee10_shouldPassEveryFilterThrough() { + when(employeeMasterInter.getEmployeeDetails11(77, 29, 3, 11, "dr.mehta", 3117)) + .thenReturn(new ArrayList<>(List.of(userDetail(3117, "dr.mehta")))); + + assertSuccessContaining(controller.searchEmployee10("{\"serviceProviderID\":77,\"pSMStateID\":29," + + "\"serviceID\":3,\"roleID\":11,\"userName\":\"dr.mehta\",\"userID\":3117}"), "dr.mehta"); + verify(employeeMasterInter).getEmployeeDetails11(77, 29, 3, 11, "dr.mehta", 3117); + } + + @Test + @DisplayName("searchEmployee10 should answer an error envelope when the search fails") + void searchEmployee10_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeDetails11(any(), any(), any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchEmployee10("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getAgentID should answer a success envelope without reaching a service") + void getAgentID_shouldAnswerSuccessEnvelope() { + assertGenericFailure(controller.getAgentID("{}")); + } + + @Test + @DisplayName("getAllTitle should answer every title on record") + void getAllTitle_shouldAnswerEveryTitle() { + M_Title title = new M_Title(); + title.setTitleID(1); + title.setTitleName("Dr"); + when(employeeMasterInter.getAllTitle()).thenReturn(new ArrayList<>(List.of(title))); + + assertSuccessContaining(controller.getAllTitle("{}"), "Dr"); + } + + @Test + @DisplayName("getAllTitle should answer an error envelope when the lookup fails") + void getAllTitle_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getAllTitle()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllTitle("{}")); + } + + @Test + @DisplayName("getAllGender should answer every gender on record") + void getAllGender_shouldAnswerEveryGender() { + M_Gender gender = new M_Gender(); + gender.setGenderID(1); + gender.setGenderName("Female"); + when(employeeMasterInter.getAllGender()).thenReturn(new ArrayList<>(List.of(gender))); + + assertSuccessContaining(controller.getAllGender("{}"), "Female"); + } + + @Test + @DisplayName("getAllGender should answer an error envelope when the lookup fails") + void getAllGender_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getAllGender()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllGender("{}")); + } + + @Test + @DisplayName("getAlllocation should resolve the mapping before reading the office details") + void getAlllocation_shouldResolveMappingFirst() { + M_ProviderServiceMap1 mapping = new M_ProviderServiceMap1(); + mapping.setProviderServiceMapID(4001); + Showofficedetails1 office = new Showofficedetails1(); + office.setLocationName("Bengaluru Office"); + when(employeeMasterInter.getAllByMapId2(77, 29, 3)).thenReturn(new ArrayList<>(List.of(mapping))); + when(employeeMasterInter.getlocationByMapid2(4001, 301)).thenReturn(new ArrayList<>(List.of(office))); + + assertSuccessContaining(controller.getAlllocation("{\"serviceProviderID\":77,\"stateID\":29," + + "\"serviceID\":3,\"districtID\":301}"), "Bengaluru Office"); + } + + @Test + @DisplayName("getAlllocation should fall back to no mapping when the provider has none") + void getAlllocation_shouldFallBackWithoutMapping() { + when(employeeMasterInter.getAllByMapId2(any(), any(), any())).thenReturn(new ArrayList<>()); + when(employeeMasterInter.getlocationByMapid2(0, 301)).thenReturn(new ArrayList<>()); + + assertSuccessContaining(controller.getAlllocation("{\"serviceProviderID\":77,\"districtID\":301}"), + "statusCode"); + verify(employeeMasterInter).getlocationByMapid2(0, 301); + } + + @Test + @DisplayName("getAlllocation should answer an error envelope when the lookup fails") + void getAlllocation_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getAllByMapId2(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAlllocation("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("FindEmployeeName should answer whether the user name is taken") + void findEmployeeName_shouldAnswerWhetherNameIsTaken() { + when(employeeMasterInter.FindEmployeeName("dr.mehta")).thenReturn("dr.mehta"); + + assertSuccessContaining(controller.FindEmployeeName("{\"userName\":\"dr.mehta\"}"), "dr.mehta"); + } + + @Test + @DisplayName("FindEmployeeName should answer an error envelope when the check fails") + void findEmployeeName_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.FindEmployeeName(anyString())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.FindEmployeeName("{\"userName\":\"dr.mehta\"}")); + } + + @Test + @DisplayName("Qualification should answer every qualification on record") + void qualification_shouldAnswerEveryQualification() { + M_Userqualification qualification = new M_Userqualification(); + qualification.setQualificationID(5); + qualification.setName("MBBS"); + when(employeeMasterInter.getQualification()).thenReturn(new ArrayList<>(List.of(qualification))); + + assertSuccessContaining(controller.Qualification("{}"), "MBBS"); + } + + @Test + @DisplayName("Qualification should answer an error envelope when the lookup fails") + void qualification_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getQualification()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.Qualification("{}")); + } + + @Test + @DisplayName("checkingEmpDetails should report that the identifiers are already in use") + void checkingEmpDetails_shouldReportIdentifiersInUse() { + when(employeeMasterInter.checkingEmpDetails("dr.mehta", "111122223333", "ABCDE1234F", "EMP-1", "HP-1")) + .thenReturn(Boolean.TRUE); + + assertSuccessContaining(controller.checkingEmpDetails("{\"userName\":\"dr.mehta\"," + + "\"aadhaarNo\":\"111122223333\",\"pAN\":\"ABCDE1234F\",\"employeeID\":\"EMP-1\"," + + "\"healthProfessionalID\":\"HP-1\"}"), "true"); + } + + @Test + @DisplayName("checkingEmpDetails should answer an error envelope when the check fails") + void checkingEmpDetails_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.checkingEmpDetails(any(), any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.checkingEmpDetails("{\"userName\":\"dr.mehta\"}")); + } + + @Test + @DisplayName("getDesignation should answer every designation on record") + void getDesignation_shouldAnswerEveryDesignation() { + M_Designation designation = new M_Designation(); + designation.setDesignationID(7); + designation.setDesignationName("ASHA"); + when(m_DesignationInter.getDesinationlist()).thenReturn(new ArrayList<>(List.of(designation))); + + assertSuccessContaining(controller.getDesignation("{}"), "ASHA"); + } + + @Test + @DisplayName("getDesignation should answer an error envelope when the lookup fails") + void getDesignation_shouldAnswerErrorEnvelopeOnFailure() { + when(m_DesignationInter.getDesinationlist()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getDesignation("{}")); + } + + @Test + @DisplayName("getEmployeeByDesignation should answer the employees holding the designation") + void getEmployeeByDesignation_shouldAnswerMatchingEmployees() { + when(employeeMasterInter.getEmployeeByDesiganationID(7, 77)) + .thenReturn(new ArrayList<>(List.of(user(3117, "dr.mehta")))); + + assertSuccessContaining( + controller.getEmployeeByDesignation("{\"designationID\":7,\"serviceProviderID\":77}"), "dr.mehta"); + } + + @Test + @DisplayName("getEmployeeByDesignation should answer an error envelope when the lookup fails") + void getEmployeeByDesignation_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getEmployeeByDesiganationID(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getEmployeeByDesignation("{\"designationID\":7}")); + } + + @Test + @DisplayName("completeUserDetails should answer the full user view") + void completeUserDetails_shouldAnswerFullUserView() { + when(employeeMasterInter.getcompleteUserDetails()) + .thenReturn(new ArrayList<>(List.of(showUser(3117, "dr.mehta")))); + + assertSuccessContaining(controller.completeUserDetails("{}"), "dr.mehta"); + } + + @Test + @DisplayName("completeUserDetails should answer an error envelope when the lookup fails") + void completeUserDetails_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getcompleteUserDetails()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.completeUserDetails("{}")); + } + + @Test + @DisplayName("getReligion should answer every religion on record") + void getReligion_shouldAnswerEveryReligion() { + M_Religion religion = new M_Religion(); + religion.setReligionID(1); + religion.setReligionType("Hindu"); + when(employeeMasterInter.getAllReligion()).thenReturn(new ArrayList<>(List.of(religion))); + + assertSuccessContaining(controller.getReligion("{}"), "Hindu"); + } + + @Test + @DisplayName("getReligion should answer an error envelope when the lookup fails") + void getReligion_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getAllReligion()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getReligion("{}")); + } + + @Test + @DisplayName("getCommunity should answer every community on record") + void getCommunity_shouldAnswerEveryCommunity() { + M_Community community = new M_Community(); + community.setCommunityID(1); + community.setCommunityType("General"); + when(employeeMasterInter.getAllCommunity()).thenReturn(new ArrayList<>(List.of(community))); + + assertSuccessContaining(controller.getCommunity("{}"), "General"); + } + + @Test + @DisplayName("getCommunity should answer an error envelope when the lookup fails") + void getCommunity_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getAllCommunity()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getCommunity("{}")); + } + + @Test + @DisplayName("FindEmployeeDetailsByUserName should answer the user the service resolves") + void findEmployeeDetailsByUserName_shouldAnswerResolvedUser() { + when(employeeMasterInter.FindEmployeeName1("dr.mehta")).thenReturn(user(3117, "dr.mehta")); + + assertSuccessContaining( + controller.FindEmployeeDetailsByUserName("{\"userName\":\"dr.mehta\"}"), "dr.mehta"); + } + + @Test + @DisplayName("FindEmployeeDetailsByUserName should answer an error envelope for an unknown user") + void findEmployeeDetailsByUserName_shouldAnswerErrorEnvelopeForUnknownUser() { + when(employeeMasterInter.FindEmployeeName1("dr.mehta")).thenReturn(null); + + assertCodeException(controller.FindEmployeeDetailsByUserName("{\"userName\":\"dr.mehta\"}")); + } + + @Test + @DisplayName("searchEmployee should answer a plain envelope for an empty result") + void searchEmployee_shouldAnswerPlainEnvelopeForEmptyResult() { + when(employeeMasterInter.getEmployeeDetails()).thenReturn(new ArrayList()); + + assertSuccessContaining(controller.searchEmployee("{}"), "\"statusCode\":200"); + } + + @Test + @DisplayName("getEmployeeByDesignation should read the provider from the user carrier, not the designation") + void getEmployeeByDesignation_shouldReadProviderFromUserCarrier() { + ArrayList employees = new ArrayList<>(List.of(user(3117, "dr.mehta"))); + when(employeeMasterInter.getEmployeeByDesiganationID(7, 77)).thenReturn(employees); + + controller.getEmployeeByDesignation("{\"designationID\":7,\"serviceProviderID\":77}"); + + verify(employeeMasterInter).getEmployeeByDesiganationID(7, 77); + } +} diff --git a/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterWriteControllerTest.java b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterWriteControllerTest.java new file mode 100644 index 0000000..ccdb99e --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeMasterWriteControllerTest.java @@ -0,0 +1,760 @@ +/* +* 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.admin.controller.employeemaster; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.iemr.admin.data.employeemaster.M_User1; +import com.iemr.admin.data.employeemaster.M_UserDemographics; +import com.iemr.admin.data.employeemaster.M_UserLangMapping; +import com.iemr.admin.data.employeemaster.M_UserServiceRoleMapping2; +import com.iemr.admin.data.employeemaster.V_Userservicerolemapping; +import com.iemr.admin.data.rolemaster.UserRole; + +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.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The write endpoints create and amend employees, their demographics, and the + * language and role mappings that decide what each of them may do. + */ +@DisplayName("EmployeeMasterController write Test Suite") +class EmployeeMasterWriteControllerTest extends EmployeeMasterFixture { + + private static final String ADD_EMPLOYEE_REQUEST = "{\"firstName\":\"Asha\",\"lastName\":\"Rao\"," + + "\"userName\":\"asha.rao\",\"createdBy\":\"admin\",\"titleID\":1,\"genderID\":1," + + "\"fathersName\":\"Ravi\",\"languageID\":[1,2],\"weightage\":[5,3]," + + "\"canRead\":[true,false],\"canWrite\":[true,false],\"canSpeak\":[true,true]," + + "\"previleges\":[{\"providerServiceMapID\":4001,\"workingLocationID\":401,\"roleID\":[11,12]}]}"; + + private static M_UserLangMapping langMapping(Integer id, Integer languageId) { + M_UserLangMapping mapping = new M_UserLangMapping(); + mapping.setUserLangID(id); + mapping.setLanguageID(languageId); + return mapping; + } + + @Test + @DisplayName("addEmployee should store the user, the demographics, the languages and the roles") + void addEmployee_shouldStoreEveryPartOfTheEmployee() throws Exception { + when(employeeMasterInter.saveEmployee(any())).thenReturn(3117); + when(employeeMasterInter.saveDemography(any())).thenReturn(5001); + when(employeeMasterInter.mapLanguage(anyList())).thenReturn(new ArrayList<>(List.of(langMapping(7001, 1)))); + when(employeeMasterInter.mapRole(anyList(), anyString())) + .thenReturn(new ArrayList<>(List.of(roleMapping(9001, 3117)))); + + assertSuccessContaining(controller.addEmployee(ADD_EMPLOYEE_REQUEST, request), "9001"); + + ArgumentCaptor> languages = ArgumentCaptor.forClass(List.class); + verify(employeeMasterInter).mapLanguage(languages.capture()); + assertEquals(2, languages.getValue().size(), "each language in the request must be mapped"); + assertEquals(3117, languages.getValue().get(0).getUserID()); + + ArgumentCaptor> roles = ArgumentCaptor.forClass(List.class); + verify(employeeMasterInter).mapRole(roles.capture(), anyString()); + assertEquals(4001, roles.getValue().get(0).getProviderServiceMapID()); + } + + @Test + @DisplayName("addEmployee should pass the caller's authorization on to the role mapping") + void addEmployee_shouldPassAuthorizationOn() throws Exception { + when(employeeMasterInter.saveEmployee(any())).thenReturn(3117); + when(employeeMasterInter.mapLanguage(anyList())).thenReturn(new ArrayList<>()); + when(employeeMasterInter.mapRole(anyList(), anyString())).thenReturn(new ArrayList<>()); + + controller.addEmployee(ADD_EMPLOYEE_REQUEST, request); + + verify(employeeMasterInter).mapRole(anyList(), org.mockito.ArgumentMatchers.eq(AUTH_HEADER)); + } + + @Test + @DisplayName("addEmployee should answer an error envelope for a request that names no languages") + void addEmployee_shouldAnswerErrorEnvelopeWithoutLanguages() { + when(employeeMasterInter.saveEmployee(any())).thenReturn(3117); + + assertCodeException(controller.addEmployee("{\"firstName\":\"Asha\"}", request)); + } + + @Test + @DisplayName("addEmployee should answer an error envelope when the user cannot be stored") + void addEmployee_shouldAnswerErrorEnvelopeOnStoreFailure() { + when(employeeMasterInter.saveEmployee(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.addEmployee(ADD_EMPLOYEE_REQUEST, request)); + } + + @Test + @DisplayName("editEmployee should copy the edits onto both the user and the demographics") + void editEmployee_shouldCopyEditsOntoUserAndDemographics() { + M_User1 stored = user(3117, "old.name"); + M_UserDemographics storedDemographics = demographics(3117, "old father"); + when(employeeMasterInter.editEmployee(3117)).thenReturn(stored); + when(employeeMasterInter.saveEditData(stored)).thenReturn(stored); + when(employeeMasterInter.mdedit(3117)).thenReturn(storedDemographics); + when(employeeMasterInter.saveeditDemo(storedDemographics)).thenReturn(5001); + + String response = controller.editEmployee("{\"userID\":3117,\"firstName\":\"Asha\"," + + "\"userName\":\"asha.rao\",\"fathersName\":\"Ravi\",\"pinCode\":\"560001\"}"); + + assertSuccessContaining(response, "5001"); + assertEquals("Asha", stored.getFirstName()); + assertEquals("Ravi", storedDemographics.getFathersName()); + assertEquals("560001", storedDemographics.getPinCode()); + } + + @Test + @DisplayName("editEmployee should answer an error envelope for a user that does not exist") + void editEmployee_shouldAnswerErrorEnvelopeForUnknownUser() { + when(employeeMasterInter.editEmployee(3117)).thenReturn(null); + + assertCodeException(controller.editEmployee("{\"userID\":3117}")); + } + + @Test + @DisplayName("deleteEmployee should mark the employee deleted and answer what was saved") + void deleteEmployee_shouldMarkEmployeeDeleted() { + M_User1 stored = user(3117, "asha.rao"); + when(employeeMasterInter.editEmployee(3117)).thenReturn(stored); + when(employeeMasterInter.saveEditData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteEmployee("{\"userID\":3117}"), "asha.rao"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteEmployee should answer an error envelope for a user that does not exist") + void deleteEmployee_shouldAnswerErrorEnvelopeForUnknownUser() { + when(employeeMasterInter.editEmployee(3117)).thenReturn(null); + + assertCodeException(controller.deleteEmployee("{\"userID\":3117}")); + } + + @Test + @DisplayName("updateEmployee should build one role mapping per role the request names") + void updateEmployee_shouldBuildOneMappingPerRole() { + when(employeeMasterInter.mapRoleUpdation(anyList())) + .thenReturn(new ArrayList<>(List.of(roleMapping(9001, 3117)))); + + String response = controller.updateEmployee("{\"userID\":3117,\"roleID1\":[11,12]," + + "\"providerServiceMapID\":4001,\"workingLocationID\":401,\"createdBy\":\"admin\"}"); + + assertSuccessContaining(response, "9001"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(employeeMasterInter).mapRoleUpdation(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertEquals(11, captor.getValue().get(0).getRoleID()); + assertEquals(4001, captor.getValue().get(0).getProviderServiceMapID()); + } + + @Test + @DisplayName("updateEmployee should answer an error envelope when the request names no roles") + void updateEmployee_shouldAnswerErrorEnvelopeWithoutRoles() { + assertCodeException(controller.updateEmployee("{\"userID\":3117}")); + } + + @Test + @DisplayName("deleteEmployeeRole should mark the role mapping deleted") + void deleteEmployeeRole_shouldMarkRoleMappingDeleted() { + M_UserServiceRoleMapping2 stored = roleMapping(9001, 3117); + when(employeeMasterInter.uRoledelte(9001)).thenReturn(stored); + when(employeeMasterInter.saveRoleEdit(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteEmployeeRole("{\"uSRMappingID\":9001}"), "9001"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteEmployeeRole should answer an error envelope for a mapping that does not exist") + void deleteEmployeeRole_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(employeeMasterInter.uRoledelte(9001)).thenReturn(null); + + assertCodeException(controller.deleteEmployeeRole("{\"uSRMappingID\":9001}")); + } + + @Test + @DisplayName("usrRoleAndCtiMapping should answer the summary the service reports") + void usrRoleAndCtiMapping_shouldAnswerSummary() throws Exception { + when(employeeMasterInter.mapctiAgent(anyList())).thenReturn("2 agents mapped"); + + assertSuccessContaining(controller.usrRoleAndCtiMapping("[{\"uSRMappingID\":9001,\"agentID\":\"A-1\"}]"), + "2 agents mapped"); + } + + @Test + @DisplayName("usrRoleAndCtiMapping should answer an error envelope when the mapping fails") + void usrRoleAndCtiMapping_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(employeeMasterInter.mapctiAgent(anyList())).thenThrow(new IllegalStateException("cti is unreachable")); + + assertGenericFailure(controller.usrRoleAndCtiMapping("[{\"uSRMappingID\":9001}]")); + } + + @Test + @DisplayName("ResetUserPassword should answer the outcome the service reports") + void resetUserPassword_shouldAnswerOutcome() { + when(employeeMasterInter.ResetPassword(any())).thenReturn("password reset"); + + assertSuccessContaining(controller.ResetUserPassword("{\"userID\":3117}"), "password reset"); + } + + @Test + @DisplayName("ResetUserPassword should answer an error envelope when the reset fails") + void resetUserPassword_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.ResetPassword(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.ResetUserPassword("{\"userID\":3117}")); + } + + @Test + @DisplayName("createProviderAdmin should answer the admins the service stored") + void createProviderAdmin_shouldAnswerStoredAdmins() throws Exception { + when(employeeMasterInter.createProviderAdmin(anyList())) + .thenReturn(new ArrayList<>(List.of(user(3117, "pa.user")))); + + assertSuccessContaining(controller.createProviderAdmin("[{\"userName\":\"pa.user\"}]"), "pa.user"); + } + + @Test + @DisplayName("createProviderAdmin should answer an error envelope when the store fails") + void createProviderAdmin_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(employeeMasterInter.createProviderAdmin(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createProviderAdmin("[{\"userName\":\"pa.user\"}]")); + } + + @Test + @DisplayName("getProviderAdmin should answer every provider admin on record") + void getProviderAdmin_shouldAnswerEveryAdmin() { + when(employeeMasterInter.getProviderAdmin()).thenReturn(new ArrayList<>(List.of(user(3117, "pa.user")))); + + assertSuccessContaining(controller.getProviderAdmin("{}"), "pa.user"); + } + + @Test + @DisplayName("getProviderAdmin should answer an error envelope when the lookup fails") + void getProviderAdmin_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getProviderAdmin()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getProviderAdmin("{}")); + } + + @Test + @DisplayName("editProviderAdmin should copy the edited contact details onto the stored admin") + void editProviderAdmin_shouldCopyContactDetails() { + M_User1 stored = user(3117, "pa.user"); + when(employeeMasterInter.getProviderAdminForEdit(3117)).thenReturn(stored); + when(employeeMasterInter.saveeditedData(stored)).thenReturn(stored); + + String response = controller.editProviderAdmin("{\"userID\":3117,\"firstName\":\"Asha\"," + + "\"emailID\":\"asha@example.org\",\"contactNo\":\"9000000001\",\"remarks\":\"promoted\"," + + "\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "Asha"); + assertEquals("asha@example.org", stored.getEmailID()); + assertEquals("promoted", stored.getRemarks()); + } + + @Test + @DisplayName("editProviderAdmin should answer an error envelope for an admin that does not exist") + void editProviderAdmin_shouldAnswerErrorEnvelopeForUnknownAdmin() { + when(employeeMasterInter.getProviderAdminForEdit(3117)).thenReturn(null); + + assertCodeException(controller.editProviderAdmin("{\"userID\":3117}")); + } + + @Test + @DisplayName("deleteProviderAdmin should mark the admin deleted") + void deleteProviderAdmin_shouldMarkAdminDeleted() { + M_User1 stored = user(3117, "pa.user"); + when(employeeMasterInter.getProviderAdminForEdit(3117)).thenReturn(stored); + when(employeeMasterInter.saveeditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteProviderAdmin("{\"userID\":3117,\"deleted\":true}"), "pa.user"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteProviderAdmin should answer an error envelope for an admin that does not exist") + void deleteProviderAdmin_shouldAnswerErrorEnvelopeForUnknownAdmin() { + when(employeeMasterInter.getProviderAdminForEdit(3117)).thenReturn(null); + + assertCodeException(controller.deleteProviderAdmin("{\"userID\":3117,\"deleted\":true}")); + } + + @Test + @DisplayName("createNewUser should store the demographics and register the user with the call centre") + void createNewUser_shouldStoreDemographicsAndRegisterUser() throws Exception { + M_User1 created = user(3117, "asha.rao"); + created.setCreatedBy("admin"); + when(employeeMasterInter.createNewUser(anyList())).thenReturn(new ArrayList<>(List.of(created))); + when(employeeMasterInter.SaveDemographics(any())).thenReturn(new ArrayList<>()); + + String response = controller.createNewUser( + "[{\"userName\":\"asha.rao\",\"createdBy\":\"admin\",\"fathersName\":\"Ravi\"}]", request); + + assertSuccessContaining(response, "asha.rao"); + verify(employeeMasterInter).createUserInCallCentre(created, AUTH_HEADER); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(ArrayList.class); + verify(employeeMasterInter).SaveDemographics(captor.capture()); + assertEquals("Ravi", captor.getValue().get(0).getFathersName()); + assertEquals(3117, captor.getValue().get(0).getUserID()); + } + + @Test + @DisplayName("createNewUser should answer an error envelope when the store fails") + void createNewUser_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(employeeMasterInter.createNewUser(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createNewUser("[{\"userName\":\"asha.rao\"}]", request)); + } + + @Test + @DisplayName("editUserDetails should copy the edits onto both the user and the demographics") + void editUserDetails_shouldCopyEditsOntoBoth() { + M_User1 stored = user(3117, "asha.rao"); + M_UserDemographics storedDemographics = demographics(3117, "old father"); + when(employeeMasterInter.editData(3117)).thenReturn(stored); + when(employeeMasterInter.saveeditedData(stored)).thenReturn(stored); + when(employeeMasterInter.DataByUserID(3117)).thenReturn(storedDemographics); + when(employeeMasterInter.saveeditedDemoData(storedDemographics)).thenReturn(storedDemographics); + + String response = controller.editUserDetails("{\"userID\":3117,\"firstName\":\"Asha\"," + + "\"emailID\":\"asha@example.org\",\"fathersName\":\"Ravi\",\"permPinCode\":\"560002\"," + + "\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "Ravi"); + assertEquals("Asha", stored.getFirstName()); + assertEquals(560002, storedDemographics.getPermPinCode()); + } + + @Test + @DisplayName("editUserDetails should answer an empty envelope when the user has no demographics on record") + void editUserDetails_shouldAnswerEmptyEnvelopeWithoutDemographics() { + M_User1 stored = user(3117, "asha.rao"); + when(employeeMasterInter.editData(3117)).thenReturn(stored); + when(employeeMasterInter.saveeditedData(stored)).thenReturn(stored); + when(employeeMasterInter.DataByUserID(3117)).thenReturn(null); + + assertGenericFailure(controller.editUserDetails("{\"userID\":3117,\"firstName\":\"Asha\"}")); + verify(employeeMasterInter, never()).saveeditedDemoData(any()); + } + + @Test + @DisplayName("editUserDetails should answer an error envelope for a user that does not exist") + void editUserDetails_shouldAnswerErrorEnvelopeForUnknownUser() { + when(employeeMasterInter.editData(3117)).thenReturn(null); + + assertCodeException(controller.editUserDetails("{\"userID\":3117}")); + } + + @Test + @DisplayName("deletedUserDetails should release the agent id and expire the session for a deleted user") + void deletedUserDetails_shouldReleaseAgentIdAndExpireSession() { + M_User1 stored = user(3117, "asha.rao"); + stored.setAgentID("A-1"); + M_UserDemographics storedDemographics = demographics(3117, "Ravi"); + when(employeeMasterInter.editData(3117)).thenReturn(stored); + when(employeeMasterInter.saveeditedData(stored)).thenReturn(stored); + when(employeeMasterInter.DataByUserID(3117)).thenReturn(storedDemographics); + when(employeeMasterInter.saveeditedDemoData(storedDemographics)).thenReturn(storedDemographics); + + assertSuccessContaining( + controller.deletedUserDetails("{\"userID\":3117,\"deleted\":true}", request), "Ravi"); + verify(usrAgentMappingService).updateDeletedAgentIDStatus("A-1"); + verify(employeeMasterInter).expireAuth(stored, AUTH_HEADER); + } + + @Test + @DisplayName("deletedUserDetails should leave the agent id alone when the user is only reinstated") + void deletedUserDetails_shouldLeaveAgentIdAloneWhenReinstating() { + M_User1 stored = user(3117, "asha.rao"); + M_UserDemographics storedDemographics = demographics(3117, "Ravi"); + when(employeeMasterInter.editData(3117)).thenReturn(stored); + when(employeeMasterInter.saveeditedData(stored)).thenReturn(stored); + when(employeeMasterInter.DataByUserID(3117)).thenReturn(storedDemographics); + when(employeeMasterInter.saveeditedDemoData(storedDemographics)).thenReturn(storedDemographics); + + controller.deletedUserDetails("{\"userID\":3117,\"deleted\":false}", request); + + verify(usrAgentMappingService, never()).updateDeletedAgentIDStatus(anyString()); + } + + @Test + @DisplayName("deletedUserDetails should answer an error envelope for a user that does not exist") + void deletedUserDetails_shouldAnswerErrorEnvelopeForUnknownUser() { + when(employeeMasterInter.editData(3117)).thenReturn(null); + + assertCodeException(controller.deletedUserDetails("{\"userID\":3117,\"deleted\":true}", request)); + } + + @Test + @DisplayName("searchMappedLanguageByUserId should answer the languages mapped to the user") + void searchMappedLanguageByUserId_shouldAnswerMappedLanguages() { + when(employeeMasterInter.searchMappedLangugeByUserId(3117)) + .thenReturn(new ArrayList<>(List.of(langMapping(7001, 1)))); + + assertSuccessContaining(controller.searchMappedLanguageByUserId("{\"userID\":3117}"), "7001"); + } + + @Test + @DisplayName("searchMappedLanguageByUserId should answer an error envelope when the lookup fails") + void searchMappedLanguageByUserId_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.searchMappedLangugeByUserId(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchMappedLanguageByUserId("{\"userID\":3117}")); + } + + @Test + @DisplayName("getUserMappedLanguage should answer the languages mapped under the provider") + void getUserMappedLanguage_shouldAnswerProviderLanguages() { + when(employeeMasterInter.getMappedLanguge(77)) + .thenReturn(new ArrayList<>(List.of(langMapping(7001, 1)))); + + assertSuccessContaining(controller.getUserMappedLanguage("{\"serviceProviderID\":77}"), "7001"); + } + + @Test + @DisplayName("getUserMappedLanguage should answer an error envelope when the lookup fails") + void getUserMappedLanguage_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getMappedLanguge(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getUserMappedLanguage("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("UserLangMapping should build one mapping per language in the request") + void userLangMapping_shouldBuildOneMappingPerLanguage() { + when(employeeMasterInter.mapLanguage(anyList())) + .thenReturn(new ArrayList<>(List.of(langMapping(7001, 1)))); + + String response = controller.UserLangMapping("[{\"userID\":3117,\"createdBy\":\"admin\"," + + "\"serviceProviderID\":77,\"languageID\":[1,2],\"weightage\":[5,3]," + + "\"canRead\":[true,false],\"canWrite\":[true,false],\"canSpeak\":[true,true]," + + "\"weightage_Read\":[5,3],\"weightage_Write\":[5,3],\"weightage_Speak\":[5,3]}]"); + + assertSuccessContaining(response, "7001"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(employeeMasterInter).mapLanguage(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertEquals(77, captor.getValue().get(0).getServiceProviderID()); + } + + @Test + @DisplayName("UserLangMapping should answer an error envelope when the request names no languages") + void userLangMapping_shouldAnswerErrorEnvelopeWithoutLanguages() { + assertCodeException(controller.UserLangMapping("[{\"userID\":3117}]")); + } + + @Test + @DisplayName("updateUserLanguageMapping should copy the edits onto the stored mapping") + void updateUserLanguageMapping_shouldCopyEdits() { + M_UserLangMapping stored = langMapping(7001, 1); + when(employeeMasterInter.updateLangMapping(7001)).thenReturn(stored); + when(employeeMasterInter.saveUserLangEditedData(stored)).thenReturn(stored); + + String response = controller.updateUserLanguageMapping("{\"userLangID\":7001,\"userID\":3117," + + "\"languageID\":2,\"weightage\":9,\"canRead\":true,\"canWrite\":false,\"canSpeak\":true," + + "\"weightage_Read\":9,\"weightage_Write\":1,\"weightage_Speak\":9,\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "7001"); + assertEquals(2, stored.getLanguageID()); + assertEquals(9, stored.getWeightage()); + } + + @Test + @DisplayName("updateUserLanguageMapping should answer an error envelope for a mapping that does not exist") + void updateUserLanguageMapping_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(employeeMasterInter.updateLangMapping(7001)).thenReturn(null); + + assertCodeException(controller.updateUserLanguageMapping("{\"userLangID\":7001}")); + } + + @Test + @DisplayName("UserLanguageMapping should mark the language mapping deleted") + void userLanguageMapping_shouldMarkMappingDeleted() { + M_UserLangMapping stored = langMapping(7001, 1); + when(employeeMasterInter.updateLangMapping(7001)).thenReturn(stored); + when(employeeMasterInter.saveUserLangEditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.UserLanguageMapping("{\"userLangID\":7001,\"deleted\":true}"), "7001"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("UserLanguageMapping should answer an error envelope for a mapping that does not exist") + void userLanguageMapping_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(employeeMasterInter.updateLangMapping(7001)).thenReturn(null); + + assertCodeException(controller.UserLanguageMapping("{\"userLangID\":7001,\"deleted\":true}")); + } + + @Test + @DisplayName("UserRoleMapping should build one mapping per role under each privilege") + void userRoleMapping_shouldBuildOneMappingPerRole() throws Exception { + when(employeeMasterInter.mapRole(anyList(), anyString())) + .thenReturn(new ArrayList<>(List.of(roleMapping(9001, 3117)))); + + String response = controller.UserRoleMapping("[{\"userID\":3117,\"createdBy\":\"admin\"," + + "\"serviceProviderID\":77,\"previleges\":[{\"roleID\":[11,12]," + + "\"providerServiceMapID\":4001,\"workingLocationID\":401}]}]", request); + + assertSuccessContaining(response, "9001"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(employeeMasterInter).mapRole(captor.capture(), anyString()); + assertEquals(2, captor.getValue().size()); + assertEquals(11, captor.getValue().get(0).getRoleID()); + } + + @Test + @DisplayName("UserRoleMapping should answer an error envelope when the request names no privileges") + void userRoleMapping_shouldAnswerErrorEnvelopeWithoutPrivileges() { + assertCodeException(controller.UserRoleMapping("[{\"userID\":3117}]", request)); + } + + @Test + @DisplayName("UserRoleMappings should carry the 1097 helpline flags onto the mapping") + void userRoleMappings_shouldCarryHelplineFlags() throws Exception { + when(employeeMasterInter.mapRole(anyList(), anyString())) + .thenReturn(new ArrayList<>(List.of(roleMapping(9001, 3117)))); + + String response = controller.UserRoleMappings("[{\"userID\":3117,\"createdBy\":\"admin\"," + + "\"serviceProviderID\":77,\"previleges\":[{\"providerServiceMapID\":4001," + + "\"workingLocationID\":401,\"stateID\":29,\"districtID\":301,\"blockID\":401," + + "\"blockName\":\"North\",\"facilityID\":501," + + "\"ID\":[{\"roleID\":11,\"inbound\":true,\"outbound\":false," + + "\"teleConsultation\":\"Y\"}]}]}]", request); + + assertSuccessContaining(response, "9001"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(employeeMasterInter).mapRole(captor.capture(), anyString()); + M_UserServiceRoleMapping2 built = captor.getValue().get(0); + assertEquals(11, built.getRoleID()); + assertTrue(built.getInbound()); + assertEquals("Y", built.getTeleConsultation()); + assertEquals("North", built.getBlockName()); + } + + @Test + @DisplayName("UserRoleMappings should answer an error envelope when the request names no privileges") + void userRoleMappings_shouldAnswerErrorEnvelopeWithoutPrivileges() throws Exception { + assertCodeException(controller.UserRoleMappings("[{\"userID\":3117}]", request)); + } + + @Test + @DisplayName("updateUserRoleMapping should cascade the supervisor mappings when the role changes") + void updateUserRoleMapping_shouldCascadeWhenRoleChanges() throws Exception { + M_UserServiceRoleMapping2 stored = roleMapping(9001, 3117); + stored.setRoleID(11); + when(employeeMasterInter.getDataUsrId(9001)).thenReturn(stored); + when(employeeMasterInter.saveRoleMappingeditedData(any(), anyString())).thenReturn(stored); + + String response = controller.updateUserRoleMapping("{\"uSRMappingID\":9001,\"userID\":3117," + + "\"roleID\":12,\"modifiedBy\":\"admin\"}", request); + + assertSuccessContaining(response, "9001"); + verify(employeeMasterInter).cascadeDeleteAshaMappingsForUser(3117); + assertEquals(12, stored.getRoleID()); + } + + @Test + @DisplayName("updateUserRoleMapping should cascade the supervisor mappings when the facility changes") + void updateUserRoleMapping_shouldCascadeWhenFacilityChanges() throws Exception { + M_UserServiceRoleMapping2 stored = roleMapping(9001, 3117); + stored.setRoleID(11); + stored.setFacilityID(501); + when(employeeMasterInter.getDataUsrId(9001)).thenReturn(stored); + when(employeeMasterInter.saveRoleMappingeditedData(any(), anyString())).thenReturn(stored); + + controller.updateUserRoleMapping("{\"uSRMappingID\":9001,\"userID\":3117,\"roleID\":11," + + "\"facilityID\":502}", request); + + verify(employeeMasterInter).cascadeDeleteAshaMappingsForUser(3117); + } + + @Test + @DisplayName("updateUserRoleMapping should leave the supervisor mappings alone when nothing changed") + void updateUserRoleMapping_shouldLeaveSupervisorMappingsAlone() throws Exception { + M_UserServiceRoleMapping2 stored = roleMapping(9001, 3117); + stored.setRoleID(11); + stored.setFacilityID(501); + when(employeeMasterInter.getDataUsrId(9001)).thenReturn(stored); + when(employeeMasterInter.saveRoleMappingeditedData(any(), anyString())).thenReturn(stored); + + controller.updateUserRoleMapping("{\"uSRMappingID\":9001,\"userID\":3117,\"roleID\":11," + + "\"facilityID\":501}", request); + + verify(employeeMasterInter, never()).cascadeDeleteAshaMappingsForUser(anyInt()); + } + + @Test + @DisplayName("updateUserRoleMapping should carry the optional call flags only when the request sets them") + void updateUserRoleMapping_shouldCarryOptionalFlagsOnlyWhenSet() throws Exception { + M_UserServiceRoleMapping2 stored = roleMapping(9001, 3117); + stored.setRoleID(11); + stored.setInbound(Boolean.TRUE); + stored.setTeleConsultation("Y"); + when(employeeMasterInter.getDataUsrId(9001)).thenReturn(stored); + when(employeeMasterInter.saveRoleMappingeditedData(any(), anyString())).thenReturn(stored); + + controller.updateUserRoleMapping("{\"uSRMappingID\":9001,\"userID\":3117,\"roleID\":11}", request); + + assertTrue(stored.getInbound(), "an unset flag must keep the value already on record"); + assertEquals("Y", stored.getTeleConsultation()); + } + + @Test + @DisplayName("updateUserRoleMapping should answer an error envelope for a mapping that does not exist") + void updateUserRoleMapping_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(employeeMasterInter.getDataUsrId(9001)).thenReturn(null); + + assertCodeException(controller.updateUserRoleMapping("{\"uSRMappingID\":9001}", request)); + } + + @Test + @DisplayName("deleteUserRoleMapping should cascade the supervisor mappings before marking it deleted") + void deleteUserRoleMapping_shouldCascadeBeforeDeleting() throws Exception { + M_UserServiceRoleMapping2 stored = roleMapping(9001, 3117); + when(employeeMasterInter.getDataUsrId(9001)).thenReturn(stored); + when(employeeMasterInter.saveRoleMappingeditedData(any(), anyString())).thenReturn(stored); + + assertSuccessContaining( + controller.deleteUserRoleMapping("{\"uSRMappingID\":9001,\"deleted\":true}", request), "9001"); + verify(employeeMasterInter).cascadeDeleteAshaMappingsForDeactivation(stored); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteUserRoleMapping should not cascade when the mapping is being reinstated") + void deleteUserRoleMapping_shouldNotCascadeWhenReinstating() throws Exception { + M_UserServiceRoleMapping2 stored = roleMapping(9001, 3117); + stored.setDeleted(Boolean.TRUE); + when(employeeMasterInter.getDataUsrId(9001)).thenReturn(stored); + when(employeeMasterInter.saveRoleMappingeditedData(any(), anyString())).thenReturn(stored); + + controller.deleteUserRoleMapping("{\"uSRMappingID\":9001,\"deleted\":false}", request); + + verify(employeeMasterInter, never()).cascadeDeleteAshaMappingsForDeactivation(any()); + } + + @Test + @DisplayName("deleteUserRoleMapping should answer an error envelope for a mapping that does not exist") + void deleteUserRoleMapping_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(employeeMasterInter.getDataUsrId(9001)).thenReturn(null); + + assertCodeException(controller.deleteUserRoleMapping("{\"uSRMappingID\":9001,\"deleted\":true}", request)); + } + + @Test + @DisplayName("getUserRoleMapped should answer the mapped roles for the provider") + void getUserRoleMapped_shouldAnswerMappedRoles() { + V_Userservicerolemapping mapped = new V_Userservicerolemapping(); + mapped.setuSRMappingID(9001); + mapped.setName("Asha Rao"); + when(employeeMasterInter.getMappedRole(77)).thenReturn(new ArrayList<>(List.of(mapped))); + + assertSuccessContaining(controller.getUserRoleMapped("{\"serviceProviderID\":77}"), "Asha Rao"); + } + + @Test + @DisplayName("getUserRoleMapped should answer an error envelope when the lookup fails") + void getUserRoleMapped_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getMappedRole(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getUserRoleMapped("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("searchMappedRoleByNameorUserId should answer the roles matching the name or id") + void searchMappedRoleByNameorUserId_shouldAnswerMatchingRoles() { + V_Userservicerolemapping mapped = new V_Userservicerolemapping(); + mapped.setuSRMappingID(9001); + mapped.setName("Asha Rao"); + when(employeeMasterInter.getMappedRole("Asha Rao", 3117)).thenReturn(new ArrayList<>(List.of(mapped))); + + assertSuccessContaining( + controller.searchMappedRoleByNameorUserId("{\"name\":\"Asha Rao\",\"userID\":3117}"), "Asha Rao"); + } + + @Test + @DisplayName("searchMappedRoleByNameorUserId should answer an error envelope when the search fails") + void searchMappedRoleByNameorUserId_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getMappedRole(anyString(), anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchMappedRoleByNameorUserId("{\"name\":\"Asha Rao\",\"userID\":3117}")); + } + + @Test + @DisplayName("getUserRoleTM should answer the telemedicine roles the service resolves") + void getUserRoleTM_shouldAnswerTelemedicineRoles() { + UserRole role = new UserRole(); + role.setRoleID(11); + role.setRolename("Specialist"); + when(employeeMasterInter.getUserRoleTM(any())).thenReturn(new ArrayList<>(List.of(role))); + + assertSuccessContaining(controller.getUserRoleTM("{\"userID\":3117}"), "Specialist"); + } + + @Test + @DisplayName("getUserRoleTM should answer an error envelope when the lookup fails") + void getUserRoleTM_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeMasterInter.getUserRoleTM(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getUserRoleTM("{\"userID\":3117}")); + } + + @Test + @DisplayName("deleteUserRoleMappingTM should answer the mapping the service retired") + void deleteUserRoleMappingTM_shouldAnswerRetiredMapping() throws Exception { + when(employeeMasterInter.deleteuserrolemapTM(any())).thenReturn(roleMapping(9001, 3117)); + + assertSuccessContaining( + controller.deleteUserRoleMappingTM("{\"uSRMappingID\":9001,\"deleted\":true}", request), "9001"); + } + + @Test + @DisplayName("deleteUserRoleMappingTM should answer an error envelope when the retirement fails") + void deleteUserRoleMappingTM_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(employeeMasterInter.deleteuserrolemapTM(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.deleteUserRoleMappingTM("{\"uSRMappingID\":9001}", request)); + } +} diff --git a/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeSignatureControllerTest.java b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeSignatureControllerTest.java new file mode 100644 index 0000000..87a59d5 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/employeemaster/EmployeeSignatureControllerTest.java @@ -0,0 +1,238 @@ +/* +* 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.admin.controller.employeemaster; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.json.JSONObject; +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 org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; + +import com.iemr.admin.data.employeemaster.EmployeeSignature; +import com.iemr.admin.service.employeemaster.EmployeeSignatureServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +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.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.when; + +/** + * A user's signature is stamped onto the prescriptions they sign, so the upload, + * download and activation endpoints all guard a clinically meaningful artefact. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("EmployeeSignatureController Test Suite") +class EmployeeSignatureControllerTest { + + private static final byte[] SIGNATURE = "a-png-body".getBytes(StandardCharsets.UTF_8); + + @Mock + private EmployeeSignatureServiceImpl employeeSignatureServiceImpl; + + @InjectMocks + private EmployeeSignatureController controller; + + private static EmployeeSignature signature(String fileName, String fileType) { + EmployeeSignature signature = new EmployeeSignature(); + signature.setUserID(3117L); + signature.setFileName(fileName); + signature.setFileType(fileType); + signature.setSignature(SIGNATURE); + return signature; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + @Test + @DisplayName("uploadFile should decode the uploaded body and answer the stored signature id") + void uploadFile_shouldDecodeAndStore() { + EmployeeSignature uploaded = new EmployeeSignature(); + uploaded.setUserID(3117L); + uploaded.setFileContent(Base64.getEncoder().encodeToString(SIGNATURE)); + when(employeeSignatureServiceImpl.uploadSignature(any())).thenReturn(9001L); + + String response = controller.uploadFile(uploaded); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("9001"), response); + assertArrayEquals(SIGNATURE, uploaded.getSignature(), + "the base64 body must be decoded before it is stored"); + } + + @Test + @DisplayName("uploadFile should answer an error envelope for a body that is not valid base64") + void uploadFile_shouldAnswerErrorEnvelopeForInvalidBody() { + EmployeeSignature uploaded = new EmployeeSignature(); + uploaded.setFileContent("not base 64 !!"); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.uploadFile(uploaded))); + } + + @Test + @DisplayName("uploadFile should answer an error envelope when the store fails") + void uploadFile_shouldAnswerErrorEnvelopeOnStoreFailure() { + EmployeeSignature uploaded = new EmployeeSignature(); + uploaded.setFileContent(Base64.getEncoder().encodeToString(SIGNATURE)); + when(employeeSignatureServiceImpl.uploadSignature(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.uploadFile(uploaded))); + } + + @Test + @DisplayName("fetchFile should answer the signature as an attachment of its own type") + void fetchFile_shouldAnswerSignatureAsAttachment() throws Exception { + when(employeeSignatureServiceImpl.fetchSignature(3117L)) + .thenReturn(signature("asha-signature.png", MediaType.IMAGE_PNG_VALUE)); + + ResponseEntity response = controller.fetchFile(3117L); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(MediaType.IMAGE_PNG, response.getHeaders().getContentType()); + assertArrayEquals(SIGNATURE, response.getBody()); + assertTrue(response.getHeaders().getContentDisposition().toString().contains("asha-signature.png")); + } + + @Test + @DisplayName("fetchFile should fall back to a plain download for a file type it cannot parse") + void fetchFile_shouldFallBackForUnparseableType() throws Exception { + when(employeeSignatureServiceImpl.fetchSignature(3117L)) + .thenReturn(signature("asha-signature.png", "not/a/media/type")); + + ResponseEntity response = controller.fetchFile(3117L); + + assertEquals(MediaType.APPLICATION_OCTET_STREAM, response.getHeaders().getContentType()); + } + + @Test + @DisplayName("fetchFile should fall back to a plain download when no file type is on record") + void fetchFile_shouldFallBackWithoutAType() throws Exception { + when(employeeSignatureServiceImpl.fetchSignature(3117L)) + .thenReturn(signature("asha-signature.png", null)); + + assertEquals(MediaType.APPLICATION_OCTET_STREAM, controller.fetchFile(3117L).getHeaders().getContentType()); + } + + @Test + @DisplayName("fetchFile should raise rather than answer an empty download for a missing signature") + void fetchFile_shouldRaiseForMissingSignature() { + when(employeeSignatureServiceImpl.fetchSignature(3117L)).thenReturn(null); + + Exception thrown = assertThrows(Exception.class, () -> controller.fetchFile(3117L)); + assertTrue(thrown.getMessage().contains("Error while downloading file"), thrown.getMessage()); + } + + @Test + @DisplayName("existFile should report both that a signature exists and whether it is active") + void existFile_shouldReportExistenceAndActivation() throws Exception { + when(employeeSignatureServiceImpl.existSignature(3117L)).thenReturn(Boolean.TRUE); + when(employeeSignatureServiceImpl.isSignatureActive(3117L)).thenReturn(Boolean.TRUE); + + String response = controller.existFile(3117L); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("\"signStatus\":\"true\""), response); + assertTrue(response.contains("\"response\":\"true\""), response); + } + + @Test + @DisplayName("existFile should report a signature that exists but has been deactivated") + void existFile_shouldReportDeactivatedSignature() throws Exception { + when(employeeSignatureServiceImpl.existSignature(3117L)).thenReturn(Boolean.TRUE); + when(employeeSignatureServiceImpl.isSignatureActive(3117L)).thenReturn(Boolean.FALSE); + + assertTrue(controller.existFile(3117L).contains("\"signStatus\":\"false\"")); + } + + @Test + @DisplayName("existFile should answer an error envelope when the check fails") + void existFile_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(employeeSignatureServiceImpl.existSignature(anyLong())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.existFile(3117L))); + } + + @Test + @DisplayName("ActivateUser should report the signature as active once it is reinstated") + void activateUser_shouldReportSignatureActive() { + EmployeeSignature updated = signature("asha-signature.png", MediaType.IMAGE_PNG_VALUE); + updated.setDeleted(Boolean.FALSE); + when(employeeSignatureServiceImpl.updateUserSignatureStatus(anyString())).thenReturn(updated); + + String response = controller.ActivateUser("{\"userID\":3117,\"deleted\":false}", + new MockHttpServletRequest()); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("\"active\":true"), response); + } + + @Test + @DisplayName("ActivateUser should report the signature as inactive once it is deactivated") + void activateUser_shouldReportSignatureInactive() { + EmployeeSignature updated = signature("asha-signature.png", MediaType.IMAGE_PNG_VALUE); + updated.setDeleted(Boolean.TRUE); + when(employeeSignatureServiceImpl.updateUserSignatureStatus(anyString())).thenReturn(updated); + + assertTrue(controller.ActivateUser("{\"userID\":3117,\"deleted\":true}", new MockHttpServletRequest()) + .contains("\"active\":false")); + } + + @Test + @DisplayName("ActivateUser should treat a signature with no flag on record as inactive") + void activateUser_shouldTreatMissingFlagAsInactive() { + EmployeeSignature updated = signature("asha-signature.png", MediaType.IMAGE_PNG_VALUE); + when(employeeSignatureServiceImpl.updateUserSignatureStatus(anyString())).thenReturn(updated); + + assertTrue(controller.ActivateUser("{\"userID\":3117}", new MockHttpServletRequest()) + .contains("\"active\":false")); + } + + @Test + @DisplayName("ActivateUser should answer an error envelope when the change fails") + void activateUser_shouldAnswerErrorEnvelopeOnFailure() { + when(employeeSignatureServiceImpl.updateUserSignatureStatus(anyString())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.ActivateUser("{\"userID\":3117}", new MockHttpServletRequest()))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/facilitytype/FacilitytypeControllerTest.java b/src/test/java/com/iemr/admin/controller/facilitytype/FacilitytypeControllerTest.java new file mode 100644 index 0000000..0dce546 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/facilitytype/FacilitytypeControllerTest.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.admin.controller.facilitytype; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.facilitytype.M_facilitytype; +import com.iemr.admin.data.store.M_Facility; +import com.iemr.admin.data.store.M_FacilityLevel; +import com.iemr.admin.repository.store.MainStoreRepo; +import com.iemr.admin.service.facilitytype.M_facilitytypeInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The facility type endpoints define the levels of the health facility + * hierarchy, so a type still in use may not be retired out from under the + * facilities that carry it. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("FacilitytypeController Test Suite") +class FacilitytypeControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer TYPE_ID = 3; + + @Mock + private M_facilitytypeInter m_facilitytypeInter; + + @Mock + private MainStoreRepo mainStoreRepo; + + @InjectMocks + private FacilitytypeController controller; + + private static M_facilitytype type(Integer id, String name) { + M_facilitytype type = new M_facilitytype(); + type.setFacilityTypeID(id); + type.setFacilityTypeName(name); + return type; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("getFacility should answer the facility types of the provider") + void getFacility_shouldAnswerProviderTypes() { + when(m_facilitytypeInter.getAllFicilityData(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(type(TYPE_ID, "PHC")))); + + assertSuccessContaining(controller.getFacility("{\"providerServiceMapID\":4001}"), "PHC"); + } + + @Test + @DisplayName("getFacility should answer an error envelope when the lookup fails") + void getFacility_shouldAnswerErrorEnvelopeOnFailure() { + when(m_facilitytypeInter.getAllFicilityData(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getFacility("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("addFacility should answer the facility types the service stored") + void addFacility_shouldAnswerStoredTypes() { + when(m_facilitytypeInter.addAllFicilityData(anyList())) + .thenReturn(new ArrayList<>(List.of(type(TYPE_ID, "PHC")))); + + assertSuccessContaining(controller.addFacility("[{\"facilityTypeName\":\"PHC\"}]"), "PHC"); + } + + @Test + @DisplayName("addFacility should answer an error envelope when the store fails") + void addFacility_shouldAnswerErrorEnvelopeOnFailure() { + when(m_facilitytypeInter.addAllFicilityData(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.addFacility("[{\"facilityTypeName\":\"PHC\"}]")); + } + + @Test + @DisplayName("editFacility should copy only the fields the request actually sets") + void editFacility_shouldCopyOnlySuppliedFields() { + M_facilitytype stored = type(TYPE_ID, "old name"); + stored.setRuralUrban("Rural"); + stored.setFacilityTypeDesc("old description"); + when(m_facilitytypeInter.editAllFicilityData(TYPE_ID)).thenReturn(stored); + when(m_facilitytypeInter.updateFacilityData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editFacility( + "{\"facilityTypeID\":3,\"facilityTypeName\":\"PHC\",\"modifiedBy\":\"admin\"}"), "PHC"); + assertEquals("Rural", stored.getRuralUrban(), "an unset field keeps the value on record"); + assertEquals("old description", stored.getFacilityTypeDesc()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("editFacility should copy the rural-urban split and description when the request sets them") + void editFacility_shouldCopySuppliedSplitAndDescription() { + M_facilitytype stored = type(TYPE_ID, "old name"); + when(m_facilitytypeInter.editAllFicilityData(TYPE_ID)).thenReturn(stored); + when(m_facilitytypeInter.updateFacilityData(stored)).thenReturn(stored); + + controller.editFacility("{\"facilityTypeID\":3,\"ruralUrban\":\"Urban\"," + + "\"facilityTypeDesc\":\"Urban primary centre\",\"modifiedBy\":\"admin\"}"); + + assertEquals("Urban", stored.getRuralUrban()); + assertEquals("Urban primary centre", stored.getFacilityTypeDesc()); + } + + @Test + @DisplayName("editFacility should answer an error envelope for a facility type that does not exist") + void editFacility_shouldAnswerErrorEnvelopeForUnknownType() { + when(m_facilitytypeInter.editAllFicilityData(TYPE_ID)).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.editFacility("{\"facilityTypeID\":3}"))); + } + + @Test + @DisplayName("deleteFacility should refuse to retire a facility type facilities still carry") + void deleteFacility_shouldRefuseTypeInUse() { + when(m_facilitytypeInter.editAllFicilityData(TYPE_ID)).thenReturn(type(TYPE_ID, "PHC")); + when(mainStoreRepo.findByFacilityTypeIDAndDeletedFalse(TYPE_ID)) + .thenReturn(List.of(new M_Facility(), new M_Facility())); + + String response = controller.deleteFacility("{\"facilityTypeID\":3,\"deleted\":true}"); + + assertGenericFailure(response); + assertTrue(response.contains("in use by 2 active facilities"), response); + } + + @Test + @DisplayName("deleteFacility should retire a facility type nothing carries") + void deleteFacility_shouldRetireUnusedType() { + M_facilitytype stored = type(TYPE_ID, "PHC"); + when(m_facilitytypeInter.editAllFicilityData(TYPE_ID)).thenReturn(stored); + when(mainStoreRepo.findByFacilityTypeIDAndDeletedFalse(TYPE_ID)).thenReturn(new ArrayList<>()); + when(m_facilitytypeInter.updateFacilityData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteFacility("{\"facilityTypeID\":3,\"deleted\":true}"), "PHC"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteFacility should reinstate a facility type without checking what carries it") + void deleteFacility_shouldReinstateWithoutChecking() { + M_facilitytype stored = type(TYPE_ID, "PHC"); + stored.setDeleted(Boolean.TRUE); + when(m_facilitytypeInter.editAllFicilityData(TYPE_ID)).thenReturn(stored); + when(m_facilitytypeInter.updateFacilityData(stored)).thenReturn(stored); + + controller.deleteFacility("{\"facilityTypeID\":3,\"deleted\":false}"); + + verify(mainStoreRepo, never()).findByFacilityTypeIDAndDeletedFalse(anyInt()); + } + + @Test + @DisplayName("checkFacilityTypeCode should report whether the code is already taken") + void checkFacilityTypeCode_shouldReportWhetherCodeIsTaken() { + when(m_facilitytypeInter.checkFacilityTypeCode(any())).thenReturn(Boolean.TRUE); + + assertSuccessContaining(controller.checkFacilityTypeCode("{\"facilityTypeCode\":\"PHC-1\"}"), "true"); + } + + @Test + @DisplayName("checkFacilityTypeCode should answer an error envelope when the check fails") + void checkFacilityTypeCode_shouldAnswerErrorEnvelopeOnFailure() { + when(m_facilitytypeInter.checkFacilityTypeCode(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.checkFacilityTypeCode("{\"facilityTypeCode\":\"PHC-1\"}")); + } + + @Test + @DisplayName("getFacilityTypesByRuralUrban should narrow the types to the split the caller names") + void getByRuralUrban_shouldNarrowToSplit() { + when(m_facilitytypeInter.getFacilityTypesByRuralUrban(PSM_ID, "Rural")) + .thenReturn(new ArrayList<>(List.of(type(TYPE_ID, "PHC")))); + + assertSuccessContaining(controller.getFacilityTypesByRuralUrban( + "{\"providerServiceMapID\":4001,\"ruralUrban\":\"Rural\"}"), "PHC"); + } + + @Test + @DisplayName("getFacilityTypesByRuralUrban should answer an error envelope when the lookup fails") + void getByRuralUrban_shouldAnswerErrorEnvelopeOnFailure() { + when(m_facilitytypeInter.getFacilityTypesByRuralUrban(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getFacilityTypesByRuralUrban("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("getFacilityLevels should answer the hierarchy levels on record") + void getFacilityLevels_shouldAnswerHierarchyLevels() { + M_FacilityLevel level = new M_FacilityLevel(); + when(m_facilitytypeInter.getFacilityLevels()).thenReturn(new ArrayList<>(List.of(level))); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(controller.getFacilityLevels())); + } + + @Test + @DisplayName("getFacilityLevels should answer an error envelope when the lookup fails") + void getFacilityLevels_shouldAnswerErrorEnvelopeOnFailure() { + when(m_facilitytypeInter.getFacilityLevels()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getFacilityLevels()); + } + + @Test + @DisplayName("getFacilityTypesByBlock should answer the types used in the block") + void getByBlock_shouldAnswerBlockTypes() { + when(m_facilitytypeInter.getFacilityTypesByBlock(401)) + .thenReturn(new ArrayList<>(List.of(type(TYPE_ID, "PHC")))); + + assertSuccessContaining(controller.getFacilityTypesByBlock("{\"blockID\":401}"), "PHC"); + } + + @Test + @DisplayName("getFacilityTypesByBlock should answer an error envelope when the lookup fails") + void getByBlock_shouldAnswerErrorEnvelopeOnFailure() { + when(m_facilitytypeInter.getFacilityTypesByBlock(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getFacilityTypesByBlock("{\"blockID\":401}")); + } + + @Test + @DisplayName("getFacilityTypesByState should answer the types used in the state") + void getByState_shouldAnswerStateTypes() { + when(m_facilitytypeInter.getFacilityTypesByState(29)) + .thenReturn(new ArrayList<>(List.of(type(TYPE_ID, "PHC")))); + + assertSuccessContaining(controller.getFacilityTypesByState("{\"stateID\":29}"), "PHC"); + } + + @Test + @DisplayName("getFacilityTypesByState should answer an error envelope when the lookup fails") + void getByState_shouldAnswerErrorEnvelopeOnFailure() { + when(m_facilitytypeInter.getFacilityTypesByState(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getFacilityTypesByState("{\"stateID\":29}")); + } + + @Test + @DisplayName("checkFacilityTypeName should report whether the name is already used in the state") + void checkFacilityTypeName_shouldReportWhetherNameIsUsed() { + when(m_facilitytypeInter.checkFacilityTypeNameExists("PHC", 29)).thenReturn(true); + + assertSuccessContaining( + controller.checkFacilityTypeName("{\"facilityTypeName\":\"PHC\",\"stateID\":29}"), "true"); + } + + @Test + @DisplayName("checkFacilityTypeName should answer an error envelope when the check fails") + void checkFacilityTypeName_shouldAnswerErrorEnvelopeOnFailure() { + when(m_facilitytypeInter.checkFacilityTypeNameExists(anyString(), anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.checkFacilityTypeName("{\"facilityTypeName\":\"PHC\",\"stateID\":29}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/foetalmonitormaster/FoetalMonitorControllerTest.java b/src/test/java/com/iemr/admin/controller/foetalmonitormaster/FoetalMonitorControllerTest.java new file mode 100644 index 0000000..caf6063 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/foetalmonitormaster/FoetalMonitorControllerTest.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.admin.controller.foetalmonitormaster; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.foetalmonitormaster.FoetalMonitorDeviceID; +import com.iemr.admin.service.foetalmonitormaster.FoetalMonitorService; +import com.iemr.admin.utils.exception.IEMRException; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The fetosense endpoints keep the device catalogue and the pairing between a + * device and the van it travels in. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("FoetalMonitorController Test Suite") +class FoetalMonitorControllerTest { + + private static final Integer PSM_ID = 4001; + private static final String AUTH = "sess-0d5f3a7c"; + private static final String DEVICE_JSON = "{\"vfdID\":9001,\"deviceID\":\"FS-1\",\"vanID\":71," + + "\"providerServiceMapID\":4001,\"deactivated\":false}"; + + @Mock + private FoetalMonitorService foetalMonitorService; + + @InjectMocks + private FoetalMonitorController controller; + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + private static void assertInvalidRequest(String response) { + assertEquals(OutputResponse.USERID_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("createFoetalMonitorTestMaster should answer the tests the service stored") + void createTestMaster_shouldAnswerStoredTests() throws Exception { + when(foetalMonitorService.createFoetalMonitorTestMaster(anyString())) + .thenReturn("[{\"testName\":\"Non stress test\"}]"); + + assertSuccessContaining(controller.createFoetalMonitorTestMaster("[{}]"), "Non stress test"); + } + + @Test + @DisplayName("createFoetalMonitorTestMaster should stay at its default when the service stored nothing") + void createTestMaster_shouldStayAtDefaultWhenNothingStored() throws Exception { + when(foetalMonitorService.createFoetalMonitorTestMaster(anyString())).thenReturn(null); + + assertGenericFailure(controller.createFoetalMonitorTestMaster("[{}]")); + } + + @Test + @DisplayName("createFoetalMonitorTestMaster should answer an error envelope when the store fails") + void createTestMaster_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(foetalMonitorService.createFoetalMonitorTestMaster(anyString())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createFoetalMonitorTestMaster("[{}]")); + } + + @Test + @DisplayName("fetchFoetalMonitorTestMaster should answer the tests of the provider") + void fetchTestMaster_shouldAnswerProviderTests() { + when(foetalMonitorService.getFoetalMonitorTestMaster(PSM_ID)) + .thenReturn("[{\"testName\":\"Non stress test\"}]"); + + assertSuccessContaining(controller.fetchFoetalMonitorTestMaster(PSM_ID), "Non stress test"); + } + + @Test + @DisplayName("fetchFoetalMonitorTestMaster should refuse a request that names no provider") + void fetchTestMaster_shouldRefuseRequestWithoutProvider() { + assertInvalidRequest(controller.fetchFoetalMonitorTestMaster(0)); + verify(foetalMonitorService, never()).getFoetalMonitorTestMaster(anyInt()); + } + + @Test + @DisplayName("fetchFoetalMonitorTestMaster should answer an error envelope when the lookup fails") + void fetchTestMaster_shouldAnswerErrorEnvelopeOnFailure() { + when(foetalMonitorService.getFoetalMonitorTestMaster(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.fetchFoetalMonitorTestMaster(PSM_ID)); + } + + @Test + @DisplayName("updateProcedureMaster should answer the test once the edit lands") + void updateTestMaster_shouldAnswerEditedTest() { + when(foetalMonitorService.updateFoetalMonitorTestMaster(anyString())) + .thenReturn("{\"testName\":\"Non stress test\"}"); + + assertSuccessContaining(controller.updateProcedureMaster("{}"), "Non stress test"); + } + + @Test + @DisplayName("updateProcedureMaster should report an edit the service could not apply") + void updateTestMaster_shouldReportUnappliedEdit() { + when(foetalMonitorService.updateFoetalMonitorTestMaster(anyString())).thenReturn(null); + + String response = controller.updateProcedureMaster("{}"); + + assertInvalidRequest(response); + assertTrue(response.contains("Failed to update procedure details"), response); + } + + @Test + @DisplayName("updateProcedureStatus should answer the test once its status has changed") + void updateTestStatus_shouldAnswerChangedTest() throws Exception { + when(foetalMonitorService.updateFoetalMonitorTestMasterStatus(11, true)) + .thenReturn("{\"testName\":\"Non stress test\"}"); + + assertSuccessContaining( + controller.updateProcedureStatus("{\"foetalMonitorTestID\":11,\"deleted\":true}"), + "Non stress test"); + } + + @Test + @DisplayName("updateProcedureStatus should report a status the service could not change") + void updateTestStatus_shouldReportUnchangedStatus() throws Exception { + when(foetalMonitorService.updateFoetalMonitorTestMasterStatus(anyInt(), anyBoolean())).thenReturn(null); + + assertInvalidRequest(controller.updateProcedureStatus("{\"foetalMonitorTestID\":11,\"deleted\":true}")); + } + + @Test + @DisplayName("updateProcedureStatus should refuse a request that names no test") + void updateTestStatus_shouldRefuseRequestWithoutTest() { + assertInvalidRequest(controller.updateProcedureStatus("{\"deleted\":true}")); + } + + @Test + @DisplayName("saveFoetalMonitorDeviceID should report the devices it stored") + void saveDeviceID_shouldReportStoredDevices() throws Exception { + when(foetalMonitorService.saveFoetalMonitorDeviceID(any())).thenReturn(1); + + assertSuccessContaining( + controller.saveFoetalMonitorDeviceID(new ArrayList<>(List.of(new FoetalMonitorDeviceID())), AUTH), + "Device ID saved successfully"); + } + + @Test + @DisplayName("saveFoetalMonitorDeviceID should report the reason the service refused the device") + void saveDeviceID_shouldReportRefusal() throws Exception { + when(foetalMonitorService.saveFoetalMonitorDeviceID(any())) + .thenThrow(new IEMRException("Error in saving foetal monitor device ID")); + + String response = controller.saveFoetalMonitorDeviceID(new ArrayList<>(), AUTH); + + assertGenericFailure(response); + assertTrue(response.contains("Error in saving foetal monitor device ID"), response); + } + + @Test + @DisplayName("saveVanIDandDeviceIDMapping should report a pairing the service accepted") + void saveMapping_shouldReportAcceptedPairing() throws Exception { + when(foetalMonitorService.vanIDAndDeviceIDMapping(any())).thenReturn(1); + + assertSuccessContaining(controller.saveVanIDandDeviceIDMapping(DEVICE_JSON, AUTH), + "Mapping Done successfully"); + } + + @Test + @DisplayName("saveVanIDandDeviceIDMapping should report the reason the service refused the pairing") + void saveMapping_shouldReportRefusal() throws Exception { + when(foetalMonitorService.vanIDAndDeviceIDMapping(any())) + .thenThrow(new IEMRException("Error in updating the VanID")); + + assertGenericFailure(controller.saveVanIDandDeviceIDMapping(DEVICE_JSON, AUTH)); + } + + @Test + @DisplayName("getFoetalMonitorDeviceID should answer the devices the service publishes") + void getDeviceID_shouldAnswerPublishedDevices() throws Exception { + when(foetalMonitorService.getFoetalMonitorDeviceID(any())) + .thenReturn("{\"fetosenseDeviceIDs\":[{\"deviceID\":\"FS-1\"}]}"); + + assertSuccessContaining(controller.getFoetalMonitorDeviceID(DEVICE_JSON), "FS-1"); + } + + @Test + @DisplayName("getFoetalMonitorDeviceID should report the reason the lookup failed") + void getDeviceID_shouldReportFailure() throws Exception { + when(foetalMonitorService.getFoetalMonitorDeviceID(any())) + .thenThrow(new IEMRException("Error in getting foetal monitor DeviceID")); + + assertGenericFailure(controller.getFoetalMonitorDeviceID(DEVICE_JSON)); + } + + @Test + @DisplayName("getVanIDAndDeviceID should answer the vans and devices still free to pair") + void getVanAndDevice_shouldAnswerFreePairs() throws Exception { + when(foetalMonitorService.getvanIDAndFoetalMonitorDeviceID(any())) + .thenReturn("{\"VanIDs\":[],\"deviceIDs\":[]}"); + + assertSuccessContaining(controller.getVanIDAndDeviceID(DEVICE_JSON), "VanIDs"); + } + + @Test + @DisplayName("getVanIDAndDeviceID should report the reason the lookup failed") + void getVanAndDevice_shouldReportFailure() throws Exception { + when(foetalMonitorService.getvanIDAndFoetalMonitorDeviceID(any())) + .thenThrow(new IEMRException("Error in getting vanID and foetalMonitorID")); + + assertGenericFailure(controller.getVanIDAndDeviceID(DEVICE_JSON)); + } + + @Test + @DisplayName("getMappedWorklist should answer the pairings on record") + void getWorklist_shouldAnswerPairings() throws Exception { + when(foetalMonitorService.getVanIDMappingWorklist(any())) + .thenReturn("[{\"deviceID\":\"FS-1\"}]"); + + assertSuccessContaining(controller.getMappedWorklist(DEVICE_JSON), "FS-1"); + } + + @Test + @DisplayName("getMappedWorklist should report the reason the lookup failed") + void getWorklist_shouldReportFailure() throws Exception { + when(foetalMonitorService.getVanIDMappingWorklist(any())) + .thenThrow(new IEMRException("Error in getting vanID mapping worklist")); + + assertGenericFailure(controller.getMappedWorklist(DEVICE_JSON)); + } + + @Test + @DisplayName("updateFoetalMonitorDeviceID should report a device the service saved") + void updateDeviceID_shouldReportSavedDevice() throws Exception { + when(foetalMonitorService.updateFoetalMonitorDeviceID(any())).thenReturn(1); + + assertSuccessContaining(controller.updateFoetalMonitorDeviceID(DEVICE_JSON, AUTH), + "DeviceID updated successfully"); + } + + @Test + @DisplayName("updateFoetalMonitorDeviceID should report the reason the service refused the edit") + void updateDeviceID_shouldReportRefusal() throws Exception { + when(foetalMonitorService.updateFoetalMonitorDeviceID(any())) + .thenThrow(new IEMRException("Error in updating the Device ID")); + + String response = controller.updateFoetalMonitorDeviceID(DEVICE_JSON, AUTH); + + assertGenericFailure(response); + assertTrue(response.contains("Unable to update deviceID"), response); + } + + @Test + @DisplayName("deleteFoetalMonitorDeviceID should report a device the service retired") + void deleteDeviceID_shouldReportRetiredDevice() throws Exception { + when(foetalMonitorService.deleteFoetalMonitorDeviceID(any())).thenReturn(1); + + assertSuccessContaining(controller.deleteFoetalMonitorDeviceID(DEVICE_JSON, AUTH), + "Device ID de-activated successfully"); + } + + @Test + @DisplayName("deleteFoetalMonitorDeviceID should report the reason the service refused the retirement") + void deleteDeviceID_shouldReportRefusal() throws Exception { + when(foetalMonitorService.deleteFoetalMonitorDeviceID(any())) + .thenThrow(new IEMRException("Error in de-activating the device ID")); + + assertGenericFailure(controller.deleteFoetalMonitorDeviceID(DEVICE_JSON, AUTH)); + } + + @Test + @DisplayName("updateMapping should report a pairing the service moved") + void updateMapping_shouldReportMovedPairing() throws Exception { + when(foetalMonitorService.updatingvanIDAndDeviceIDMapping(any())).thenReturn(1); + + assertSuccessContaining(controller.updateMapping(DEVICE_JSON, AUTH), "Mapping updated successfully"); + } + + @Test + @DisplayName("updateMapping should report the reason the service refused to move the pairing") + void updateMapping_shouldReportRefusal() throws Exception { + when(foetalMonitorService.updatingvanIDAndDeviceIDMapping(any())) + .thenThrow(new IEMRException("Error in updating van details")); + + assertGenericFailure(controller.updateMapping(DEVICE_JSON, AUTH)); + } + + @Test + @DisplayName("deleteVanIDAndFoetalMonitorDeviceID should report a pairing the service released") + void deleteMapping_shouldReportReleasedPairing() throws Exception { + when(foetalMonitorService.deleteVanIDAndDeviceIDMapping(any())).thenReturn(1); + + assertSuccessContaining(controller.deleteVanIDAndFoetalMonitorDeviceID(DEVICE_JSON, AUTH), + "Mapped deactivated successfully"); + } + + @Test + @DisplayName("deleteVanIDAndFoetalMonitorDeviceID should report the reason the service refused the release") + void deleteMapping_shouldReportRefusal() throws Exception { + when(foetalMonitorService.deleteVanIDAndDeviceIDMapping(any())) + .thenThrow(new IEMRException("The Van is already mapped with a device")); + + String response = controller.deleteVanIDAndFoetalMonitorDeviceID(DEVICE_JSON, AUTH); + + assertGenericFailure(response); + assertTrue(response.contains("already mapped with a device"), response); + } +} diff --git a/src/test/java/com/iemr/admin/controller/health/HealthControllerTest.java b/src/test/java/com/iemr/admin/controller/health/HealthControllerTest.java new file mode 100644 index 0000000..a8fcd38 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/health/HealthControllerTest.java @@ -0,0 +1,102 @@ +/* +* 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.admin.controller.health; + +import java.util.LinkedHashMap; +import java.util.Map; + +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.HttpStatus; +import org.springframework.http.ResponseEntity; + +import com.iemr.admin.service.health.HealthService; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +/** + * Monitoring reads the HTTP status of this endpoint, so a degraded deployment + * has to stay a 200 while an unreachable one becomes a 503. + */ +@ExtendWith(MockitoExtension.class) +@DisplayName("HealthController Test Suite") +class HealthControllerTest { + + @Mock + private HealthService healthService; + + private static Map health(String status) { + Map health = new LinkedHashMap<>(); + health.put("status", status); + health.put("checkedAt", "2026-02-17T09:30:00Z"); + return health; + } + + @Test + @DisplayName("checkHealth should answer 200 for a deployment that is up") + void checkHealth_shouldAnswerOkWhenUp() { + when(healthService.checkHealth()).thenReturn(health("UP")); + + ResponseEntity> response = new HealthController(healthService).checkHealth(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("UP", response.getBody().get("status")); + } + + @Test + @DisplayName("checkHealth should answer 200 for a deployment that is degraded but serving") + void checkHealth_shouldAnswerOkWhenDegraded() { + when(healthService.checkHealth()).thenReturn(health("DEGRADED")); + + ResponseEntity> response = new HealthController(healthService).checkHealth(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("DEGRADED", response.getBody().get("status")); + } + + @Test + @DisplayName("checkHealth should answer 503 for a deployment that is down") + void checkHealth_shouldAnswerUnavailableWhenDown() { + when(healthService.checkHealth()).thenReturn(health("DOWN")); + + ResponseEntity> response = new HealthController(healthService).checkHealth(); + + assertEquals(HttpStatus.SERVICE_UNAVAILABLE, response.getStatusCode()); + } + + @Test + @DisplayName("checkHealth should answer 503 rather than propagate a failure of the check itself") + void checkHealth_shouldAnswerUnavailableWhenCheckFails() { + when(healthService.checkHealth()).thenThrow(new IllegalStateException("diagnostics unavailable")); + + ResponseEntity> response = new HealthController(healthService).checkHealth(); + + assertEquals(HttpStatus.SERVICE_UNAVAILABLE, response.getStatusCode()); + assertEquals("DOWN", response.getBody().get("status")); + assertNotNull(response.getBody().get("timestamp")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/item/ItemControllerTest.java b/src/test/java/com/iemr/admin/controller/item/ItemControllerTest.java new file mode 100644 index 0000000..e4fb8b1 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/item/ItemControllerTest.java @@ -0,0 +1,545 @@ +/* +* 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.admin.controller.item; + +import java.util.List; + +import org.json.JSONObject; +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.admin.data.items.CodeChecker; +import com.iemr.admin.data.items.ItemMaster; +import com.iemr.admin.data.items.M_ItemCategory; +import com.iemr.admin.data.items.M_ItemForm; +import com.iemr.admin.data.items.M_Route; +import com.iemr.admin.service.item.ItemService; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The item endpoints keep the inventory catalogue: the drugs and consumables a + * facility can issue, and the categories, forms and routes that classify them. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ItemController Test Suite") +class ItemControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer ITEM_ID = 101; + + @Mock + private ItemService itemService; + + @InjectMocks + private ItemController controller; + + private static ItemMaster item(Integer id, String name) { + ItemMaster item = new ItemMaster(); + item.setItemID(id); + item.setItemName(name); + return item; + } + + private static M_ItemCategory category(Integer id, String name) { + M_ItemCategory category = new M_ItemCategory(); + category.setItemCategoryID(id); + category.setItemCategoryName(name); + return category; + } + + private static M_ItemForm form(Integer id, String name) { + M_ItemForm form = new M_ItemForm(); + form.setItemFormID(id); + form.setItemForm(name); + return form; + } + + private static M_Route route(Integer id, String name) { + M_Route route = new M_Route(); + route.setRouteID(id); + route.setRouteName(name); + return route; + } + + private static CodeChecker codeChecker(String name, String code) { + CodeChecker checker = new CodeChecker(); + checker.setName(name); + checker.setCode(code); + checker.setProviderServiceMapID(PSM_ID); + return checker; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("getItemForm should answer the item forms configured for the provider") + void getItemForm_shouldAnswerConfiguredForms() { + when(itemService.getItemFormProviderServiceMapID(PSM_ID)).thenReturn(List.of(form(11, "Tablet"))); + + assertSuccessContaining(controller.getItemForm(PSM_ID), "Tablet"); + } + + @Test + @DisplayName("getItemForm should answer an error envelope when the lookup fails") + void getItemForm_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.getItemFormProviderServiceMapID(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getItemForm(PSM_ID)); + } + + @Test + @DisplayName("getItemRoute should answer the routes configured for the provider") + void getItemRoute_shouldAnswerConfiguredRoutes() { + when(itemService.getItemRouteProviderServiceMapID(PSM_ID)).thenReturn(List.of(route(21, "Oral"))); + + assertSuccessContaining(controller.getItemRoute(PSM_ID), "Oral"); + } + + @Test + @DisplayName("getItemRoute should answer an error envelope when the lookup fails") + void getItemRoute_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.getItemRouteProviderServiceMapID(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getItemRoute(PSM_ID)); + } + + @Test + @DisplayName("getItemCategory should ask for every category when the caller sends zero") + void getItemCategory_shouldAskForEveryCategoryOnZero() { + when(itemService.getItemCategory(true, PSM_ID)).thenReturn(List.of(category(31, "Drugs"))); + + assertSuccessContaining(controller.getItemCategory(PSM_ID, 0), "Drugs"); + verify(itemService).getItemCategory(true, PSM_ID); + } + + @Test + @DisplayName("getItemCategory should ask for the live categories only for any other flag") + void getItemCategory_shouldAskForLiveCategoriesOtherwise() { + when(itemService.getItemCategory(false, PSM_ID)).thenReturn(List.of(category(31, "Drugs"))); + + assertSuccessContaining(controller.getItemCategory(PSM_ID, 1), "Drugs"); + verify(itemService).getItemCategory(false, PSM_ID); + } + + @Test + @DisplayName("getItemCategory should answer an error envelope when the lookup fails") + void getItemCategory_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.getItemCategory(anyBoolean(), anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getItemCategory(PSM_ID, 0)); + } + + @Test + @DisplayName("createItemMaster should answer the items the service stored") + void createItemMaster_shouldAnswerStoredItems() { + when(itemService.addAllItemMaster(anyList())).thenReturn(List.of(item(ITEM_ID, "Paracetamol"))); + + assertSuccessContaining(controller.createItemMaster(new ItemMaster[] { item(null, "Paracetamol") }), + "Paracetamol"); + } + + @Test + @DisplayName("createItemMaster should answer an error envelope when the store fails") + void createItemMaster_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.addAllItemMaster(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createItemMaster(new ItemMaster[] { item(null, "Paracetamol") })); + } + + @Test + @DisplayName("getItemMaster should answer the catalogue of the provider") + void getItemMaster_shouldAnswerProviderCatalogue() { + when(itemService.getItemMaster(PSM_ID)).thenReturn(List.of(item(ITEM_ID, "Paracetamol"))); + + assertSuccessContaining(controller.getItemMaster(PSM_ID), "Paracetamol"); + } + + @Test + @DisplayName("getItemMaster should answer an error envelope when the lookup fails") + void getItemMaster_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.getItemMaster(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getItemMaster(PSM_ID)); + } + + @Test + @DisplayName("blockItemMaster should answer how many items the service blocked") + void blockItemMaster_shouldAnswerBlockedCount() { + when(itemService.blockItemMaster(ITEM_ID, true)).thenReturn(1); + + assertSuccessContaining(controller.blockItemMaster(ITEM_ID, true), "1"); + } + + @Test + @DisplayName("blockItemMaster should answer an error envelope when the block fails") + void blockItemMaster_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.blockItemMaster(anyInt(), anyBoolean())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.blockItemMaster(ITEM_ID, true)); + } + + @Test + @DisplayName("discontinueItemMaster should answer how many items the service discontinued") + void discontinueItemMaster_shouldAnswerDiscontinuedCount() { + when(itemService.discontinueItemMaster(ITEM_ID, true)).thenReturn(1); + + assertSuccessContaining(controller.discontinueItemMaster(ITEM_ID, true), "1"); + } + + @Test + @DisplayName("discontinueItemMaster should answer an error envelope when the change fails") + void discontinueItemMaster_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.discontinueItemMaster(anyInt(), anyBoolean())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.discontinueItemMaster(ITEM_ID, true)); + } + + @Test + @DisplayName("editItemMaster should copy the edited fields onto the stored item") + void editItemMaster_shouldCopyEditedFields() { + ItemMaster stored = item(ITEM_ID, "Paracetamol"); + ItemMaster request = item(ITEM_ID, "Paracetamol"); + request.setIsMedical(Boolean.TRUE); + request.setItemCategoryID(31); + request.setPharmacologyCategoryID(41); + request.setManufacturerID(51); + request.setIsScheduledDrug(Boolean.FALSE); + request.setItemDesc("Antipyretic"); + request.setSctCode("SCT-1"); + request.setSctTerm("Paracetamol 500mg"); + request.setModifiedBy("admin"); + when(itemService.getItemMasterByID(ITEM_ID)).thenReturn(stored); + when(itemService.createItemMaster(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editItemMaster(request), "Paracetamol"); + assertEquals("Antipyretic", stored.getItemDesc()); + assertEquals("SCT-1", stored.getSctCode()); + assertEquals(31, stored.getItemCategoryID()); + } + + @Test + @DisplayName("editItemMaster should answer an error envelope for an item that does not exist") + void editItemMaster_shouldAnswerErrorEnvelopeForUnknownItem() { + when(itemService.getItemMasterByID(ITEM_ID)).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.editItemMaster(item(ITEM_ID, "x")))); + } + + @Test + @DisplayName("configItemIssue should answer how many categories the service reconfigured") + void configItemIssue_shouldAnswerReconfiguredCount() { + when(itemService.updateItemIssueConfig(anyList())).thenReturn(2); + + assertSuccessContaining(controller.configItemIssue(new M_ItemCategory[] { category(31, "Drugs") }), "2"); + } + + @Test + @DisplayName("configItemIssue should answer an error envelope when the change fails") + void configItemIssue_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.updateItemIssueConfig(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.configItemIssue(new M_ItemCategory[] { category(31, "Drugs") })); + } + + @Test + @DisplayName("configexpiryalert should answer how many categories had their alert window changed") + void configexpiryalert_shouldAnswerChangedCount() { + when(itemService.updateExpiryAlert(anyList())).thenReturn(1); + + assertSuccessContaining(controller.configexpiryalert(new M_ItemCategory[] { category(31, "Drugs") }), "1"); + } + + @Test + @DisplayName("configexpiryalert should answer an error envelope when the change fails") + void configexpiryalert_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.updateExpiryAlert(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.configexpiryalert(new M_ItemCategory[] { category(31, "Drugs") })); + } + + @Test + @DisplayName("getItem should answer the items in the category") + void getItem_shouldAnswerItemsInCategory() { + when(itemService.getItemMasters(PSM_ID, 31)).thenReturn(List.of(item(ITEM_ID, "Paracetamol"))); + + assertSuccessContaining( + controller.getItem("{\"providerServiceMapID\":4001,\"itemCategoryID\":31}"), "Paracetamol"); + } + + @Test + @DisplayName("getItem should answer an error envelope when the lookup fails") + void getItem_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.getItemMasters(any(), any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getItem("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("createItemCategories should report success only when the service stored something") + void createItemCategories_shouldReportOutcome() { + when(itemService.createItemCategories(anyList())).thenReturn(1); + assertSuccessContaining(controller.createItemCategories(new M_ItemCategory[] { category(null, "Drugs") }), + "Item Categories saved successfully"); + + when(itemService.createItemCategories(anyList())).thenReturn(0); + assertSuccessContaining(controller.createItemCategories(new M_ItemCategory[] { category(null, "Drugs") }), + "Failed to store Item Categories"); + } + + @Test + @DisplayName("createItemCategories should answer an error envelope when the store fails") + void createItemCategories_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.createItemCategories(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createItemCategories(new M_ItemCategory[] { category(null, "Drugs") })); + } + + @Test + @DisplayName("editItemCategory should report success only when the service changed something") + void editItemCategory_shouldReportOutcome() { + when(itemService.editItemCategory(any())).thenReturn(1); + assertSuccessContaining(controller.editItemCategory(category(31, "Drugs")), + "Item Category updated successfully"); + + when(itemService.editItemCategory(any())).thenReturn(0); + assertSuccessContaining(controller.editItemCategory(category(31, "Drugs")), + "Failed to update Item Category"); + } + + @Test + @DisplayName("editItemCategory should answer an error envelope when the change fails") + void editItemCategory_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.editItemCategory(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.editItemCategory(category(31, "Drugs"))); + } + + @Test + @DisplayName("blockItemCategory should report success only when the service blocked something") + void blockItemCategory_shouldReportOutcome() { + when(itemService.blockItemCategory(any())).thenReturn(1); + assertSuccessContaining(controller.blockItemCategory(category(31, "Drugs")), + "Item Category blocked successfully"); + + when(itemService.blockItemCategory(any())).thenReturn(0); + assertSuccessContaining(controller.blockItemCategory(category(31, "Drugs")), + "Failed to block Item Category"); + } + + @Test + @DisplayName("blockItemCategory should answer an error envelope when the block fails") + void blockItemCategory_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.blockItemCategory(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.blockItemCategory(category(31, "Drugs"))); + } + + @Test + @DisplayName("createItemForms should report success only when the service stored something") + void createItemForms_shouldReportOutcome() { + when(itemService.createItemForms(anyList())).thenReturn(1); + assertSuccessContaining(controller.createItemForms(new M_ItemForm[] { form(null, "Tablet") }), + "Item Forms saved successfully"); + + when(itemService.createItemForms(anyList())).thenReturn(0); + assertSuccessContaining(controller.createItemForms(new M_ItemForm[] { form(null, "Tablet") }), + "Failed to store Item Forms"); + } + + @Test + @DisplayName("createItemForms should answer an error envelope when the store fails") + void createItemForms_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.createItemForms(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createItemForms(new M_ItemForm[] { form(null, "Tablet") })); + } + + @Test + @DisplayName("editItemForm should report success only when the service changed something") + void editItemForm_shouldReportOutcome() { + when(itemService.editItemForm(any())).thenReturn(1); + assertSuccessContaining(controller.editItemForm(form(11, "Tablet")), "Item Form updated successfully"); + + when(itemService.editItemForm(any())).thenReturn(0); + assertSuccessContaining(controller.editItemForm(form(11, "Tablet")), "Failed to update Item Form"); + } + + @Test + @DisplayName("editItemForm should answer an error envelope when the change fails") + void editItemForm_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.editItemForm(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.editItemForm(form(11, "Tablet"))); + } + + @Test + @DisplayName("blockItemForm should report success only when the service blocked something") + void blockItemForm_shouldReportOutcome() { + when(itemService.blockItemForm(any())).thenReturn(1); + assertSuccessContaining(controller.blockItemForm(form(11, "Tablet")), "Item Form blocked successfully"); + + when(itemService.blockItemForm(any())).thenReturn(0); + assertSuccessContaining(controller.blockItemForm(form(11, "Tablet")), "Failed to block Item Form"); + } + + @Test + @DisplayName("blockItemForm should answer an error envelope when the block fails") + void blockItemForm_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.blockItemForm(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.blockItemForm(form(11, "Tablet"))); + } + + @Test + @DisplayName("createRoutes should report success only when the service stored something") + void createRoutes_shouldReportOutcome() { + when(itemService.createRoutes(anyList())).thenReturn(1); + assertSuccessContaining(controller.createRoutes(new M_Route[] { route(null, "Oral") }), + "Routes saved successfully"); + + when(itemService.createRoutes(anyList())).thenReturn(0); + assertSuccessContaining(controller.createRoutes(new M_Route[] { route(null, "Oral") }), + "Failed to store Routes"); + } + + @Test + @DisplayName("createRoutes should answer an error envelope when the store fails") + void createRoutes_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.createRoutes(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createRoutes(new M_Route[] { route(null, "Oral") })); + } + + @Test + @DisplayName("editRoute should report success only when the service changed something") + void editRoute_shouldReportOutcome() { + when(itemService.editRoute(any())).thenReturn(1); + assertSuccessContaining(controller.editRoute(route(21, "Oral")), "Route data updated successfully"); + + when(itemService.editRoute(any())).thenReturn(0); + assertSuccessContaining(controller.editRoute(route(21, "Oral")), "Failed to update Route data"); + } + + @Test + @DisplayName("editRoute should answer an error envelope when the change fails") + void editRoute_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.editRoute(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.editRoute(route(21, "Oral"))); + } + + @Test + @DisplayName("blockRoute should report success only when the service blocked something") + void blockRoute_shouldReportOutcome() { + when(itemService.blockRoute(any())).thenReturn(1); + assertSuccessContaining(controller.blockRoute(route(21, "Oral")), "Route blocked successfully"); + + when(itemService.blockRoute(any())).thenReturn(0); + assertSuccessContaining(controller.blockRoute(route(21, "Oral")), "Failed to block Route"); + } + + @Test + @DisplayName("blockRoute should answer an error envelope when the block fails") + void blockRoute_shouldAnswerErrorEnvelopeOnFailure() { + when(itemService.blockRoute(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.blockRoute(route(21, "Oral"))); + } + + @Test + @DisplayName("checkCode should route the check to the master the caller names") + void checkCode_shouldRouteToNamedMaster() { + when(itemService.checkCodeCategory(any())).thenReturn(Boolean.TRUE); + when(itemService.checkCodeForm(any())).thenReturn(Boolean.FALSE); + when(itemService.checkCodeItem(any())).thenReturn(Boolean.TRUE); + when(itemService.checkCodeRoute(any())).thenReturn(Boolean.FALSE); + + assertSuccessContaining(controller.blockRoute(codeChecker("itemCategory", "CAT-1")), "true"); + assertSuccessContaining(controller.blockRoute(codeChecker("itemForm", "FRM-1")), "false"); + assertSuccessContaining(controller.blockRoute(codeChecker("itemMaster", "ITM-1")), "true"); + assertSuccessContaining(controller.blockRoute(codeChecker("route", "RTE-1")), "false"); + } + + @Test + @DisplayName("checkCode should refuse a master it does not know how to check") + void checkCode_shouldRefuseUnknownMaster() { + String response = controller.blockRoute(codeChecker("something-else", "X-1")); + + assertGenericFailure(response); + assertTrue(response.contains("Failed to check code for something-else"), response); + verify(itemService, never()).checkCodeCategory(any()); + } + + @Test + @DisplayName("checkCode should refuse a request that leaves out what to check") + void checkCode_shouldRefuseIncompleteRequest() { + CodeChecker incomplete = new CodeChecker(); + incomplete.setName("itemCategory"); + + String response = controller.blockRoute(incomplete); + + assertGenericFailure(response); + assertTrue(response.contains("Name, Code and ProviderServiceMapID is mandatory"), response); + } + + @Test + @DisplayName("checkCode should refuse a request that names no provider mapping") + void checkCode_shouldRefuseRequestWithoutProviderMapping() { + CodeChecker incomplete = codeChecker("itemCategory", "CAT-1"); + incomplete.setProviderServiceMapID(0); + + assertGenericFailure(controller.blockRoute(incomplete)); + } +} diff --git a/src/test/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingControllerTest.java b/src/test/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingControllerTest.java new file mode 100644 index 0000000..85bbc0a --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingControllerTest.java @@ -0,0 +1,259 @@ +/* +* 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.admin.controller.itemfacilitymapping; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.items.ItemInStore; +import com.iemr.admin.data.itemfacilitymapping.M_itemfacilitymapping; +import com.iemr.admin.data.itemfacilitymapping.V_fetchItemFacilityMap; +import com.iemr.admin.service.itemfacilitymapping.M_itemfacilitymappingInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The item-facility endpoints decide which items each store may issue, so an + * unmapped item is one the store cannot dispense. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("MItemFacilityMappingController Test Suite") +class MItemFacilityMappingControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer FACILITY_ID = 501; + + @Mock + private M_itemfacilitymappingInter M_itemfacilitymappingInter; + + @InjectMocks + private MItemFacilityMappingController controller; + + private static M_itemfacilitymapping mapping(Integer id, Integer itemId) { + M_itemfacilitymapping mapping = new M_itemfacilitymapping(); + mapping.setItemStoreMapID(id); + mapping.setItemID(itemId); + mapping.setFacilityID(FACILITY_ID); + return mapping; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + private static void assertCodeException(String response) { + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(response), response); + } + + @Test + @DisplayName("mapItemtoStrore should create one mapping per item the request lists") + void mapItemtoStrore_shouldCreateOneMappingPerItem() { + when(M_itemfacilitymappingInter.mapItemtoStore(anyList())) + .thenReturn(new ArrayList<>(List.of(mapping(9001, 101)))); + + String response = controller.mapItemtoStrore("[{\"facilityID\":501,\"mappingType\":\"Store\"," + + "\"providerServiceMapID\":4001,\"status\":\"Active\",\"createdBy\":\"admin\"," + + "\"itemID1\":[101,102]}]"); + + assertSuccessContaining(response, "9001"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(M_itemfacilitymappingInter).mapItemtoStore(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertEquals(FACILITY_ID, captor.getValue().get(0).getFacilityID()); + } + + @Test + @DisplayName("mapItemtoStrore should answer an error envelope when no items are listed") + void mapItemtoStrore_shouldAnswerErrorEnvelopeWithoutItems() { + assertCodeException(controller.mapItemtoStrore("[{\"facilityID\":501}]")); + } + + @Test + @DisplayName("editItemtoStrore should copy the edits onto the stored mapping") + void editItemtoStrore_shouldCopyEdits() { + M_itemfacilitymapping stored = mapping(9001, 101); + when(M_itemfacilitymappingInter.editdata(9001)).thenReturn(stored); + when(M_itemfacilitymappingInter.saveEditedItem(stored)).thenReturn(stored); + + String response = controller.editItemtoStrore("{\"itemFacilityMapID\":9001,\"facilityID\":502," + + "\"itemID\":102,\"mappingType\":\"Sub Store\",\"providerServiceMapID\":4001," + + "\"status\":\"Inactive\"}"); + + assertSuccessContaining(response, "9001"); + assertEquals(502, stored.getFacilityID()); + assertEquals("Sub Store", stored.getMappingType()); + assertEquals("Inactive", stored.getStatus()); + } + + @Test + @DisplayName("editItemtoStrore should answer an error envelope for a mapping that does not exist") + void editItemtoStrore_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(M_itemfacilitymappingInter.editdata(9001)).thenReturn(null); + + assertCodeException(controller.editItemtoStrore("{\"itemFacilityMapID\":9001}")); + } + + @Test + @DisplayName("deleteItemtoStrore should mark the mapping deleted") + void deleteItemtoStrore_shouldMarkMappingDeleted() { + M_itemfacilitymapping stored = mapping(9001, 101); + when(M_itemfacilitymappingInter.editdata(9001)).thenReturn(stored); + when(M_itemfacilitymappingInter.saveEditedItem(stored)).thenReturn(stored); + + assertSuccessContaining( + controller.deleteItemtoStrore("{\"itemFacilityMapID\":9001,\"deleted\":true}"), "9001"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteItemtoStrore should answer an error envelope for a mapping that does not exist") + void deleteItemtoStrore_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(M_itemfacilitymappingInter.editdata(9001)).thenReturn(null); + + assertCodeException(controller.deleteItemtoStrore("{\"itemFacilityMapID\":9001,\"deleted\":true}")); + } + + @Test + @DisplayName("getSubStroreitem should answer the items mapped to the sub store") + void getSubStroreitem_shouldAnswerMappedItems() { + when(M_itemfacilitymappingInter.getsubitemforsubStote(PSM_ID, FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(mapping(9001, 101)))); + + assertSuccessContaining( + controller.getSubStroreitem("{\"providerServiceMapID\":4001,\"facilityID\":501}"), "9001"); + } + + @Test + @DisplayName("getSubStroreitem should answer an error envelope when the lookup fails") + void getSubStroreitem_shouldAnswerErrorEnvelopeOnFailure() { + when(M_itemfacilitymappingInter.getsubitemforsubStote(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getSubStroreitem("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("getAllFacilityMappedData should answer the mappings of the provider") + void getAllFacilityMappedData_shouldAnswerProviderMappings() { + V_fetchItemFacilityMap view = new V_fetchItemFacilityMap(); + view.setItemFacilityMapID(9001); + when(M_itemfacilitymappingInter.getAllFacilityMappedData(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(view))); + + assertSuccessContaining( + controller.getAllFacilityMappedData("{\"providerServiceMapID\":4001}"), "9001"); + } + + @Test + @DisplayName("getAllFacilityMappedData should answer an error envelope when the lookup fails") + void getAllFacilityMappedData_shouldAnswerErrorEnvelopeOnFailure() { + when(M_itemfacilitymappingInter.getAllFacilityMappedData(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllFacilityMappedData("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("getItemMappingsByFacility should answer the mappings of the facility") + void getItemMappingsByFacility_shouldAnswerFacilityMappings() { + V_fetchItemFacilityMap view = new V_fetchItemFacilityMap(); + view.setItemFacilityMapID(9001); + when(M_itemfacilitymappingInter.getItemMappingsByFacilityID(FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(view))); + + assertSuccessContaining(controller.getItemMappingsByFacility("{\"facilityID\":501}"), "9001"); + } + + @Test + @DisplayName("getItemMappingsByFacility should answer an error envelope when the lookup fails") + void getItemMappingsByFacility_shouldAnswerErrorEnvelopeOnFailure() { + when(M_itemfacilitymappingInter.getItemMappingsByFacilityID(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getItemMappingsByFacility("{\"facilityID\":501}")); + } + + @Test + @DisplayName("getItemFromStoreID should answer the items the store holds") + void getItemFromStoreID_shouldAnswerStoreItems() { + when(M_itemfacilitymappingInter.getItemMastersFromStoreID(FACILITY_ID)) + .thenReturn(List.of(new ItemInStore(FACILITY_ID, 101, "Paracetamol", 25L))); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(controller.getItemFromStoreID(FACILITY_ID))); + } + + @Test + @DisplayName("getItemFromStoreID should answer an error envelope when the lookup fails") + void getItemFromStoreID_shouldAnswerErrorEnvelopeOnFailure() { + when(M_itemfacilitymappingInter.getItemMastersFromStoreID(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getItemFromStoreID(FACILITY_ID)); + } + + @Test + @DisplayName("deleteItemStoreMapping should answer how many mappings the service released") + void deleteItemStoreMapping_shouldAnswerReleasedCount() { + M_itemfacilitymapping request = mapping(9001, 101); + when(M_itemfacilitymappingInter.deleteItemStoreMapping(request)).thenReturn(1); + + assertSuccessContaining(controller.deleteItemStoreMapping(request), "1"); + } + + @Test + @DisplayName("deleteItemStoreMapping should answer an error envelope when the release fails") + void deleteItemStoreMapping_shouldAnswerErrorEnvelopeOnFailure() { + when(M_itemfacilitymappingInter.deleteItemStoreMapping(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.deleteItemStoreMapping(mapping(9001, 101))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/labmodule/LabModuleControllerTest.java b/src/test/java/com/iemr/admin/controller/labmodule/LabModuleControllerTest.java new file mode 100644 index 0000000..0db8a08 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/labmodule/LabModuleControllerTest.java @@ -0,0 +1,356 @@ +/* +* 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.admin.controller.labmodule; + +import org.json.JSONObject; +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 com.iemr.admin.sevice.labmodule.MastersCreationServiceImpl; +import com.iemr.admin.sevice.labmodule.MastersFetchingServiceImpl; +import com.iemr.admin.sevice.labmodule.MastersMappingServiceImpl; +import com.iemr.admin.sevice.labmodule.MastersStatusUpdateImpl; +import com.iemr.admin.utils.response.OutputResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The lab module endpoints let a provider admin define the diagnostic tests and + * their result components, and refuse a request that does not name what to act + * on. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("LabModuleController Test Suite") +class LabModuleControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer PROCEDURE_ID = 71; + private static final Integer COMPONENT_ID = 81; + private static final String PROCEDURE_JSON = "{\"procedureID\":71,\"procedureName\":\"Haemoglobin\"}"; + + @Mock + private MastersCreationServiceImpl mastersCreationServiceImpl; + + @Mock + private MastersMappingServiceImpl mastersMappingServiceImpl; + + @Mock + private MastersFetchingServiceImpl mastersFetchingServiceImpl; + + @Mock + private MastersStatusUpdateImpl mastersStatusUpdateImpl; + + private LabModuleController controller; + + @BeforeEach + void setUp() { + controller = new LabModuleController(); + controller.setMastersCreationServiceImpl(mastersCreationServiceImpl); + controller.setMastersMappingServiceImpl(mastersMappingServiceImpl); + controller.setMastersFetchingServiceImpl(mastersFetchingServiceImpl); + controller.setMastersStatusUpdateImpl(mastersStatusUpdateImpl); + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertInvalidRequest(String response) { + assertEquals(OutputResponse.USERID_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("createProcedureMaster should answer the procedure the service stored") + void createProcedureMaster_shouldAnswerStoredProcedure() throws Exception { + when(mastersCreationServiceImpl.createProcedureMaster(anyString())).thenReturn(PROCEDURE_JSON); + + assertSuccessContaining(controller.createProcedureMaster(PROCEDURE_JSON), "Haemoglobin"); + } + + @Test + @DisplayName("createProcedureMaster should stay at its default when the service stored nothing") + void createProcedureMaster_shouldStayAtDefaultWhenNothingStored() throws Exception { + when(mastersCreationServiceImpl.createProcedureMaster(anyString())).thenReturn(null); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.createProcedureMaster("{}"))); + } + + @Test + @DisplayName("createProcedureMaster should answer an error envelope when the store fails") + void createProcedureMaster_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(mastersCreationServiceImpl.createProcedureMaster(anyString())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.createProcedureMaster("{}"))); + } + + @Test + @DisplayName("createComponentMaster should answer the component the service stored") + void createComponentMaster_shouldAnswerStoredComponent() throws Exception { + when(mastersCreationServiceImpl.createComponentMaster(anyString())) + .thenReturn("{\"testComponentName\":\"Haemoglobin count\"}"); + + assertSuccessContaining(controller.createComponentMaster("{}"), "Haemoglobin count"); + } + + @Test + @DisplayName("createComponentMaster should answer an error envelope when the store fails") + void createComponentMaster_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(mastersCreationServiceImpl.createComponentMaster(anyString())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.createComponentMaster("{}"))); + } + + @Test + @DisplayName("createProcedureComponentMapping should answer the mapping the service stored") + void createMapping_shouldAnswerStoredMapping() throws Exception { + when(mastersMappingServiceImpl.createProcedureComponentMapping(anyString())) + .thenReturn("[{\"procedureName\":\"Haemoglobin\"}]"); + + assertSuccessContaining(controller.createProcedureComponentMapping("{}"), "Haemoglobin"); + } + + @Test + @DisplayName("createProcedureComponentMapping should refuse a request that maps no components") + void createMapping_shouldRefuseEmptyRequest() throws Exception { + when(mastersMappingServiceImpl.createProcedureComponentMapping(anyString())).thenReturn("1"); + + String response = controller.createProcedureComponentMapping("{}"); + + assertInvalidRequest(response); + assertTrue(response.contains("Invalid request."), response); + } + + @Test + @DisplayName("createProcedureComponentMapping should report a mapping the service could not store") + void createMapping_shouldReportUnstoredMapping() throws Exception { + when(mastersMappingServiceImpl.createProcedureComponentMapping(anyString())).thenReturn(null); + + String response = controller.createProcedureComponentMapping("{}"); + + assertInvalidRequest(response); + assertTrue(response.contains("Error while saving the data"), response); + } + + @Test + @DisplayName("createProcedureComponentMapping should answer an error envelope when the store fails") + void createMapping_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(mastersMappingServiceImpl.createProcedureComponentMapping(anyString())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.createProcedureComponentMapping("{}"))); + } + + @Test + @DisplayName("the fetch endpoints should each answer what their own service publishes") + void fetchEndpoints_shouldAnswerTheirOwnService() throws Exception { + when(mastersFetchingServiceImpl.getProcedureMaster(PSM_ID)).thenReturn("[{\"a\":1}]"); + when(mastersFetchingServiceImpl.getComponentMaster(PSM_ID)).thenReturn("[{\"b\":2}]"); + when(mastersFetchingServiceImpl.getProcedureMasterDelFalse(PSM_ID)).thenReturn("[{\"c\":3}]"); + when(mastersFetchingServiceImpl.getComponentMasterDelFalse(PSM_ID)).thenReturn("[{\"d\":4}]"); + when(mastersFetchingServiceImpl.getProcCompMappingDelFalse(PSM_ID)).thenReturn("[{\"e\":5}]"); + when(mastersFetchingServiceImpl.getProcCompMappingForProcedureID(PROCEDURE_ID)).thenReturn("[{\"f\":6}]"); + + assertSuccessContaining(controller.fetchProcedureMaster(PSM_ID), "\"a\":1"); + assertSuccessContaining(controller.fetchComponentMaster(PSM_ID), "\"b\":2"); + assertSuccessContaining(controller.fetchProcedureMasterDelFalse(PSM_ID), "\"c\":3"); + assertSuccessContaining(controller.fetchComponentMasterDelFalse(PSM_ID), "\"d\":4"); + assertSuccessContaining(controller.fetchProcCompMappingDelFalse(PSM_ID), "\"e\":5"); + assertSuccessContaining(controller.fetchProcCompMappingForSingleProcedure(PROCEDURE_ID), "\"f\":6"); + } + + @Test + @DisplayName("the fetch endpoints should refuse a request that names no record to read") + void fetchEndpoints_shouldRefuseRequestWithoutRecord() throws Exception { + assertInvalidRequest(controller.fetchProcedureMaster(0)); + assertInvalidRequest(controller.fetchComponentMaster(0)); + assertInvalidRequest(controller.fetchProcedureMasterDelFalse(0)); + assertInvalidRequest(controller.fetchComponentMasterDelFalse(0)); + assertInvalidRequest(controller.fetchProcCompMappingDelFalse(0)); + assertInvalidRequest(controller.fetchProcCompMappingForSingleProcedure(0)); + assertInvalidRequest(controller.fetchComponentDetailsForComponentID(0)); + verify(mastersFetchingServiceImpl, never()).getProcedureMaster(anyInt()); + } + + @Test + @DisplayName("fetchProcedureMaster should answer an error envelope when the lookup fails") + void fetchProcedureMaster_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(mastersFetchingServiceImpl.getProcedureMaster(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.fetchProcedureMaster(PSM_ID))); + } + + @Test + @DisplayName("fetchComponentDetailsForComponentID should answer the component the service publishes") + void fetchComponentDetails_shouldAnswerPublishedComponent() throws Exception { + when(mastersFetchingServiceImpl.getComponentDetailsForComponentID(COMPONENT_ID)) + .thenReturn("{\"testComponentName\":\"Haemoglobin count\"}"); + + assertSuccessContaining(controller.fetchComponentDetailsForComponentID(COMPONENT_ID), + "Haemoglobin count"); + } + + @Test + @DisplayName("fetchComponentDetailsForComponentID should say so when the component is not on record") + void fetchComponentDetails_shouldSaySoForUnknownComponent() throws Exception { + when(mastersFetchingServiceImpl.getComponentDetailsForComponentID(COMPONENT_ID)).thenReturn(null); + + assertSuccessContaining(controller.fetchComponentDetailsForComponentID(COMPONENT_ID), + "Component Details not found in Database."); + } + + @Test + @DisplayName("updateProcedureStatus should answer the procedure once its status has changed") + void updateProcedureStatus_shouldAnswerChangedProcedure() throws Exception { + when(mastersStatusUpdateImpl.updateProcedureStatus(PROCEDURE_ID, true)).thenReturn(PROCEDURE_JSON); + + assertSuccessContaining( + controller.updateProcedureStatus("{\"procedureID\":71,\"deleted\":true}"), "Haemoglobin"); + } + + @Test + @DisplayName("updateProcedureStatus should report a status the service could not change") + void updateProcedureStatus_shouldReportUnchangedStatus() throws Exception { + when(mastersStatusUpdateImpl.updateProcedureStatus(anyInt(), anyBoolean())).thenReturn(null); + + String response = controller.updateProcedureStatus("{\"procedureID\":71,\"deleted\":true}"); + + assertInvalidRequest(response); + assertTrue(response.contains("Failed to update the status"), response); + } + + @Test + @DisplayName("updateProcedureStatus should refuse a request that names no procedure") + void updateProcedureStatus_shouldRefuseRequestWithoutProcedure() { + assertInvalidRequest(controller.updateProcedureStatus("{\"deleted\":true}")); + assertInvalidRequest(controller.updateProcedureStatus("{\"procedureID\":0,\"deleted\":true}")); + } + + @Test + @DisplayName("updateProcedureStatus should answer an error envelope for a body it cannot read") + void updateProcedureStatus_shouldAnswerErrorEnvelopeForMalformedBody() { + assertEquals(OutputResponse.OBJECT_FAILURE, statusCodeOf(controller.updateProcedureStatus("not json"))); + } + + @Test + @DisplayName("updateComponentStatus should answer the component once its status has changed") + void updateComponentStatus_shouldAnswerChangedComponent() throws Exception { + when(mastersStatusUpdateImpl.updateComponentStatus(COMPONENT_ID, true)) + .thenReturn("{\"testComponentName\":\"Haemoglobin count\"}"); + + assertSuccessContaining( + controller.updateComponentStatus("{\"componentID\":81,\"deleted\":true}"), "Haemoglobin count"); + } + + @Test + @DisplayName("updateComponentStatus should report a status the service could not change") + void updateComponentStatus_shouldReportUnchangedStatus() throws Exception { + when(mastersStatusUpdateImpl.updateComponentStatus(anyInt(), anyBoolean())).thenReturn(null); + + assertInvalidRequest(controller.updateComponentStatus("{\"componentID\":81,\"deleted\":true}")); + } + + @Test + @DisplayName("updateComponentStatus should refuse a request that names no component") + void updateComponentStatus_shouldRefuseRequestWithoutComponent() { + assertInvalidRequest(controller.updateComponentStatus("{\"deleted\":true}")); + } + + @Test + @DisplayName("updateProcedureMaster should answer the procedure once the edit lands") + void updateProcedureMaster_shouldAnswerEditedProcedure() throws Exception { + when(mastersStatusUpdateImpl.updateProcedureMaster(anyString())).thenReturn(PROCEDURE_JSON); + + assertSuccessContaining(controller.updateProcedureMaster(PROCEDURE_JSON), "Haemoglobin"); + } + + @Test + @DisplayName("updateProcedureMaster should report an edit the service could not apply") + void updateProcedureMaster_shouldReportUnappliedEdit() throws Exception { + when(mastersStatusUpdateImpl.updateProcedureMaster(anyString())).thenReturn(null); + + String response = controller.updateProcedureMaster("{}"); + + assertInvalidRequest(response); + assertTrue(response.contains("Failed to update procedure details"), response); + } + + @Test + @DisplayName("updateProcedureMaster should answer an error envelope when the edit fails") + void updateProcedureMaster_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(mastersStatusUpdateImpl.updateProcedureMaster(anyString())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.updateProcedureMaster("{}"))); + } + + @Test + @DisplayName("updateComponentMaster should answer the component once the edit lands") + void updateComponentMaster_shouldAnswerEditedComponent() throws Exception { + when(mastersStatusUpdateImpl.updateComponentMaster(anyString())) + .thenReturn("{\"testComponentName\":\"Haemoglobin count\"}"); + + assertSuccessContaining(controller.updateComponentMaster("{}"), "Haemoglobin count"); + } + + @Test + @DisplayName("updateComponentMaster should report an edit the service could not apply") + void updateComponentMaster_shouldReportUnappliedEdit() throws Exception { + when(mastersStatusUpdateImpl.updateComponentMaster(anyString())).thenReturn(null); + + String response = controller.updateComponentMaster("{}"); + + assertInvalidRequest(response); + assertTrue(response.contains("Failed to update component details"), response); + } + + @Test + @DisplayName("updateComponentMaster should answer an error envelope when the edit fails") + void updateComponentMaster_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(mastersStatusUpdateImpl.updateComponentMaster(anyString())) + .thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.updateComponentMaster("{}"))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/labmodule/SmartDiagnosticsControllerTest.java b/src/test/java/com/iemr/admin/controller/labmodule/SmartDiagnosticsControllerTest.java new file mode 100644 index 0000000..2e181c9 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/labmodule/SmartDiagnosticsControllerTest.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.admin.controller.labmodule; + +import org.json.JSONObject; +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.springframework.test.util.ReflectionTestUtils; + +import com.iemr.admin.sevice.labmodule.IOTService; +import com.iemr.admin.utils.response.OutputResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * The smart diagnostics endpoints tell the client which device tests exist and + * where the rapid screening device is reachable. + */ +@ExtendWith(MockitoExtension.class) +@DisplayName("SmartDiagnosticsController Test Suite") +class SmartDiagnosticsControllerTest { + + @Mock + private IOTService iotService; + + @InjectMocks + private SmartDiagnosticsController controller; + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + @Test + @DisplayName("getIOTProcedure should answer the device tests the service publishes") + void getIOTProcedure_shouldAnswerDeviceTests() { + when(iotService.getIOTProcedure()).thenReturn("[{\"calibrationCode\":\"HB\"}]"); + + String response = controller.getIOTProcedure(); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("HB"), response); + } + + @Test + @DisplayName("getIOTProcedure should answer an error envelope when the lookup fails") + void getIOTProcedure_shouldAnswerErrorEnvelopeOnFailure() { + when(iotService.getIOTProcedure()).thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getIOTProcedure())); + } + + @Test + @DisplayName("getIOTComponent should answer the device components the service publishes") + void getIOTComponent_shouldAnswerDeviceComponents() { + when(iotService.getIOTComponent()).thenReturn("[{\"iotComponentID\":1}]"); + + String response = controller.getIOTComponent(); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("iotComponentID"), response); + } + + @Test + @DisplayName("getIOTComponent should answer an error envelope when the lookup fails") + void getIOTComponent_shouldAnswerErrorEnvelopeOnFailure() { + when(iotService.getIOTComponent()).thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getIOTComponent())); + } + + @Test + @DisplayName("getBiologicalScreeningDeviceUrl should answer the configured device address") + void getBiologicalScreeningDeviceUrl_shouldAnswerConfiguredAddress() { + ReflectionTestUtils.setField(controller, "biologicalScreeningDeviceUrl", "http://device.local:9000"); + + assertEquals("http://device.local:9000", controller.getBiologicalScreeningDeviceUrl()); + } +} diff --git a/src/test/java/com/iemr/admin/controller/locationmaster/LocationMasterControllerTest.java b/src/test/java/com/iemr/admin/controller/locationmaster/LocationMasterControllerTest.java new file mode 100644 index 0000000..fd267e5 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/locationmaster/LocationMasterControllerTest.java @@ -0,0 +1,425 @@ +/* +* 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.admin.controller.locationmaster; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.locationmaster.M_District; +import com.iemr.admin.data.locationmaster.M_ProviderServiceAddMapping; +import com.iemr.admin.data.locationmaster.Showofficedetails; +import com.iemr.admin.data.locationmaster.StateServiceMapping1; +import com.iemr.admin.service.locationmaster.LocationMasterServiceInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The location endpoints keep the office addresses a provider works out of, and + * choose between a national and a state-scoped lookup depending on the service + * line. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("LocationMasterController Test Suite") +class LocationMasterControllerTest { + + private static final Integer PROVIDER_ID = 77; + private static final Integer PSM_ID = 4001; + + @Mock + private LocationMasterServiceInter locationMasterServiceInter; + + @InjectMocks + private LocationMasterController controller; + + private static StateServiceMapping1 mapping(Integer psmId) { + return new StateServiceMapping1(psmId); + } + + private static Showofficedetails office(String name) { + Showofficedetails office = new Showofficedetails(); + office.setLocationName(name); + office.setProviderServiceMapID(PSM_ID); + return office; + } + + private static M_ProviderServiceAddMapping address(Integer id, String name) { + M_ProviderServiceAddMapping address = new M_ProviderServiceAddMapping(); + address.setpSAddMapID(id); + address.setLocationName(name); + return address; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + private static void assertCodeException(String response) { + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(response), response); + } + + @Test + @DisplayName("getAllRole2 should resolve the mapping before reading its addresses") + void getAllRole2_shouldResolveMappingFirst() { + when(locationMasterServiceInter.getAllByMapId2(PROVIDER_ID, 29, 3)) + .thenReturn(new ArrayList<>(List.of(mapping(PSM_ID)))); + when(locationMasterServiceInter.getlocationByMapid(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(address(51, "Bengaluru Office")))); + + assertSuccessContaining(controller.getAllRole2( + "{\"serviceProviderID\":77,\"stateID\":29,\"serviceID\":3}"), "Bengaluru Office"); + } + + @Test + @DisplayName("getAllRole2 should fall back to no mapping when the provider has none") + void getAllRole2_shouldFallBackWithoutMapping() { + when(locationMasterServiceInter.getAllByMapId2(any(), any(), any())).thenReturn(new ArrayList<>()); + when(locationMasterServiceInter.getlocationByMapid(0)).thenReturn(new ArrayList<>()); + + controller.getAllRole2("{\"serviceProviderID\":77}"); + + verify(locationMasterServiceInter).getlocationByMapid(0); + } + + @Test + @DisplayName("getAllRole2 should answer an error envelope when the lookup fails") + void getAllRole2_shouldAnswerErrorEnvelopeOnFailure() { + when(locationMasterServiceInter.getAllByMapId2(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllRole2("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getAlllocation should use the national lookup for a national service line") + void getAlllocation_shouldUseNationalLookup() { + when(locationMasterServiceInter.getAllByMapId3(PROVIDER_ID, 3)) + .thenReturn(new ArrayList<>(List.of(mapping(PSM_ID)))); + when(locationMasterServiceInter.getlocationByMapid2(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(office("Bengaluru Office")))); + + assertSuccessContaining(controller.getAlllocation( + "{\"serviceProviderID\":77,\"serviceID\":3,\"isNational\":true}"), "Bengaluru Office"); + verify(locationMasterServiceInter).getAllByMapId3(PROVIDER_ID, 3); + } + + @Test + @DisplayName("getAlllocation should use the state lookup for a state-scoped service line") + void getAlllocation_shouldUseStateLookup() { + when(locationMasterServiceInter.getAllByMapId2(PROVIDER_ID, 29, 3)) + .thenReturn(new ArrayList<>(List.of(mapping(PSM_ID)))); + when(locationMasterServiceInter.getlocationByMapid2(PSM_ID)).thenReturn(new ArrayList<>()); + + controller.getAlllocation("{\"serviceProviderID\":77,\"stateID\":29,\"serviceID\":3,\"isNational\":false}"); + + verify(locationMasterServiceInter).getAllByMapId2(PROVIDER_ID, 29, 3); + } + + @Test + @DisplayName("getAlllocation should narrow to the district when the caller names one") + void getAlllocation_shouldNarrowToDistrict() { + when(locationMasterServiceInter.getAllByMapId2(any(), any(), any())) + .thenReturn(new ArrayList<>(List.of(mapping(PSM_ID)))); + when(locationMasterServiceInter.getlocationByMapid4(PSM_ID, 301)) + .thenReturn(new ArrayList<>(List.of(office("Bengaluru Office")))); + + assertSuccessContaining(controller.getAlllocation("{\"serviceProviderID\":77,\"stateID\":29," + + "\"serviceID\":3,\"districtID\":301,\"isNational\":false}"), "Bengaluru Office"); + verify(locationMasterServiceInter, never()).getlocationByMapid2(anyInt()); + } + + @Test + @DisplayName("getAlllocation should answer an error envelope when the national flag is missing") + void getAlllocation_shouldAnswerErrorEnvelopeWithoutNationalFlag() { + assertCodeException(controller.getAlllocation("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getAlllocationNew should read the addresses straight off the mapping the caller names") + void getAlllocationNew_shouldReadFromNamedMapping() { + when(locationMasterServiceInter.getlocationByMapid2(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(office("Bengaluru Office")))); + + assertSuccessContaining(controller.getAlllocationNew("{\"providerServiceMapID\":4001}"), + "Bengaluru Office"); + } + + @Test + @DisplayName("getAlllocationNew should narrow to the district when the caller names one") + void getAlllocationNew_shouldNarrowToDistrict() { + when(locationMasterServiceInter.getlocationByMapid4(PSM_ID, 301)) + .thenReturn(new ArrayList<>(List.of(office("Bengaluru Office")))); + + assertSuccessContaining( + controller.getAlllocationNew("{\"providerServiceMapID\":4001,\"districtID\":301}"), + "Bengaluru Office"); + } + + @Test + @DisplayName("getAlllocationNew should answer an error envelope when the lookup fails") + void getAlllocationNew_shouldAnswerErrorEnvelopeOnFailure() { + when(locationMasterServiceInter.getlocationByMapid2(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAlllocationNew("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("searchRole should answer the states the provider serves") + void searchRole_shouldAnswerServedStates() { + when(locationMasterServiceInter.getStateByServiceProviderId(PROVIDER_ID)) + .thenReturn(new ArrayList<>(List.of(mapping(PSM_ID)))); + + assertSuccessContaining(controller.searchRole("{\"serviceProviderID\":77}"), "4001"); + } + + @Test + @DisplayName("searchRole should answer an error envelope when the lookup fails") + void searchRole_shouldAnswerErrorEnvelopeOnFailure() { + when(locationMasterServiceInter.getStateByServiceProviderId(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchRole("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getService should answer the service lines the provider runs in the state") + void getService_shouldAnswerServiceLinesInState() { + when(locationMasterServiceInter.getServiceByServiceProviderIdAndStateId(PROVIDER_ID, 29)) + .thenReturn(new ArrayList<>(List.of(mapping(PSM_ID)))); + + assertSuccessContaining(controller.getService("{\"serviceProviderID\":77,\"stateID\":29}"), "4001"); + } + + @Test + @DisplayName("getService should answer an error envelope when the lookup fails") + void getService_shouldAnswerErrorEnvelopeOnFailure() { + when(locationMasterServiceInter.getServiceByServiceProviderIdAndStateId(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getService("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getAllDistrict should answer the districts of the state") + void getAllDistrict_shouldAnswerStateDistricts() { + M_District district = new M_District(); + district.setDistrictID(301); + district.setDistrictName("Bengaluru Urban"); + when(locationMasterServiceInter.getAllDistrictByStateId(29)) + .thenReturn(new ArrayList<>(List.of(district))); + + assertSuccessContaining(controller.getAllDistrict("{\"stateID\":29}"), "Bengaluru Urban"); + } + + @Test + @DisplayName("getAllDistrict should answer an error envelope when the lookup fails") + void getAllDistrict_shouldAnswerErrorEnvelopeOnFailure() { + when(locationMasterServiceInter.getAllDistrictByStateId(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllDistrict("{\"stateID\":29}")); + } + + @Test + @DisplayName("getAllRole should create one address per mapping the request names") + void getAllRole_shouldCreateOneAddressPerMapping() { + when(locationMasterServiceInter.addlocation(anyList())) + .thenReturn(new ArrayList<>(List.of(address(51, "Bengaluru Office")))); + + String response = controller.getAllRole("{\"providerServiceMapID\":[4001,4002]," + + "\"address\":\"Main Road\",\"locationName\":\"Bengaluru Office\",\"districtID\":301," + + "\"createdBy\":\"admin\",\"abdmFacilityId\":\"ABDM-1\"," + + "\"abdmFacilityName\":\"Bengaluru PHC\"}"); + + assertSuccessContaining(response, "Bengaluru Office"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(locationMasterServiceInter).addlocation(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertEquals("ABDM-1", captor.getValue().get(0).getAbdmFacilityId()); + } + + @Test + @DisplayName("getAllRole should answer an error envelope when no mappings are named") + void getAllRole_shouldAnswerErrorEnvelopeWithoutMappings() { + assertCodeException(controller.getAllRole("{\"address\":\"Main Road\"}")); + } + + @Test + @DisplayName("geteditLocation should copy the edits onto the stored address") + void geteditLocation_shouldCopyEdits() { + M_ProviderServiceAddMapping stored = address(51, "old name"); + when(locationMasterServiceInter.editData(51)).thenReturn(stored); + when(locationMasterServiceInter.saveEditData(stored)).thenReturn(stored); + + String response = controller.geteditLocation("{\"pSAddMapID\":51,\"providerServiceMapID\":4001," + + "\"districtID\":301,\"address\":\"Main Road\",\"locationName\":\"Bengaluru Office\"," + + "\"abdmFacilityId\":\"ABDM-1\",\"abdmFacilityName\":\"Bengaluru PHC\"}"); + + assertSuccessContaining(response, "Bengaluru Office"); + assertEquals("Main Road", stored.getAddress()); + assertEquals("ABDM-1", stored.getAbdmFacilityId()); + } + + @Test + @DisplayName("geteditLocation should answer an error envelope for an address that does not exist") + void geteditLocation_shouldAnswerErrorEnvelopeForUnknownAddress() { + when(locationMasterServiceInter.editData(51)).thenReturn(null); + + assertCodeException(controller.geteditLocation("{\"pSAddMapID\":51}")); + } + + @Test + @DisplayName("deleteLocation should mark the address deleted") + void deleteLocation_shouldMarkAddressDeleted() { + M_ProviderServiceAddMapping stored = address(51, "Bengaluru Office"); + when(locationMasterServiceInter.editData(51)).thenReturn(stored); + when(locationMasterServiceInter.saveEditData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteLocation("{\"pSAddMapID\":51,\"deleted\":true}"), + "Bengaluru Office"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteLocation should answer an error envelope for an address that does not exist") + void deleteLocation_shouldAnswerErrorEnvelopeForUnknownAddress() { + when(locationMasterServiceInter.editData(51)).thenReturn(null); + + assertCodeException(controller.deleteLocation("{\"pSAddMapID\":51,\"deleted\":true}")); + } + + @Test + @DisplayName("getLocationByServiceID should gather the addresses of every mapping on the service line") + void getLocationByServiceID_shouldGatherAddressesAcrossMappings() { + when(locationMasterServiceInter.getLocationByServiceId(PROVIDER_ID, 3)) + .thenReturn(new ArrayList<>(List.of(mapping(4001), mapping(4002)))); + when(locationMasterServiceInter.getlocationByMapid1(any())) + .thenReturn(new ArrayList<>(List.of(office("Bengaluru Office")))); + + assertSuccessContaining( + controller.getLocationByServiceID("{\"serviceProviderID\":77,\"serviceID\":3}"), + "Bengaluru Office"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(ArrayList.class); + verify(locationMasterServiceInter).getlocationByMapid1(captor.capture()); + assertEquals(List.of(4001, 4002), captor.getValue()); + } + + @Test + @DisplayName("getLocationByServiceID should answer an error envelope when the lookup fails") + void getLocationByServiceID_shouldAnswerErrorEnvelopeOnFailure() { + when(locationMasterServiceInter.getLocationByServiceId(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getLocationByServiceID("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getLocationByStateId should gather the addresses of every mapping in the state") + void getLocationByStateId_shouldGatherAddressesAcrossMappings() { + when(locationMasterServiceInter.getLocationBySateID(PROVIDER_ID, 29)) + .thenReturn(new ArrayList<>(List.of(mapping(4001)))); + when(locationMasterServiceInter.getlocationByMapid1(any())) + .thenReturn(new ArrayList<>(List.of(office("Bengaluru Office")))); + + assertSuccessContaining( + controller.getLocationByStateId("{\"serviceProviderID\":77,\"stateID\":29}"), "Bengaluru Office"); + } + + @Test + @DisplayName("getLocationByStateId should answer an error envelope when the lookup fails") + void getLocationByStateId_shouldAnswerErrorEnvelopeOnFailure() { + when(locationMasterServiceInter.getLocationBySateID(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getLocationByStateId("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getOfficeNameByMapId should ask for one office per mapping the request lists") + void getOfficeNameByMapId_shouldAskForOneOfficePerMapping() { + when(locationMasterServiceInter.getOfficeName(any())) + .thenReturn(new ArrayList<>(List.of(office("Bengaluru Office")))); + + assertSuccessContaining( + controller.getOfficeNameByMapId("{\"providerServiceMapID\":[4001,4002]}"), "Bengaluru Office"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(ArrayList.class); + verify(locationMasterServiceInter).getOfficeName(captor.capture()); + assertEquals(2, captor.getValue().size()); + } + + @Test + @DisplayName("getOfficeNameByMapId should answer an error envelope when no mappings are listed") + void getOfficeNameByMapId_shouldAnswerErrorEnvelopeWithoutMappings() { + assertCodeException(controller.getOfficeNameByMapId("{}")); + } + + @Test + @DisplayName("getStatesByServiceID should answer the states the service line runs in") + void getStatesByServiceID_shouldAnswerServedStates() { + when(locationMasterServiceInter.getStatesByServiceId(3, PROVIDER_ID)) + .thenReturn(new ArrayList<>(List.of(mapping(PSM_ID)))); + + assertSuccessContaining( + controller.getStatesByServiceID("{\"serviceID\":3,\"serviceProviderID\":77}"), "4001"); + } + + @Test + @DisplayName("getStatesByServiceID should answer an error envelope when the lookup fails") + void getStatesByServiceID_shouldAnswerErrorEnvelopeOnFailure() { + when(locationMasterServiceInter.getStatesByServiceId(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getStatesByServiceID("{\"serviceID\":3}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/manufacturer/ManufacturerControllerTest.java b/src/test/java/com/iemr/admin/controller/manufacturer/ManufacturerControllerTest.java new file mode 100644 index 0000000..f0f3477 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/manufacturer/ManufacturerControllerTest.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.admin.controller.manufacturer; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.manufacturer.M_Manufacturer; +import com.iemr.admin.service.manufacturer.ManufacturerInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** The manufacturer master endpoints keep the manufacturer catalogue an inventory operator picks from. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ManufacturerController Test Suite") +class ManufacturerControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer RECORD_ID = 11; + + @Mock + private ManufacturerInter manufacturerInter; + + @InjectMocks + private ManufacturerController controller; + + private static M_Manufacturer record(Integer id, String name) { + M_Manufacturer record = new M_Manufacturer(); + record.setManufacturerID(id); + record.setManufacturerName(name); + return record; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("createManufacturer should answer the records the service stored") + void create_shouldAnswerStoredRecords() { + when(manufacturerInter.createManufacturer(anyList())).thenReturn(new ArrayList<>(List.of(record(RECORD_ID, "Cipla")))); + + assertSuccessContaining(controller.createManufacturer("[{\"manufacturerCode\":\"C-1\"}]"), "Cipla"); + } + + @Test + @DisplayName("createManufacturer should answer an error envelope when the store fails") + void create_shouldAnswerErrorEnvelopeOnFailure() { + when(manufacturerInter.createManufacturer(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createManufacturer("[{}]")); + } + + @Test + @DisplayName("getManufacturer should answer the records of the provider") + void get_shouldAnswerProviderRecords() { + when(manufacturerInter.createManufacturer(PSM_ID)).thenReturn(new ArrayList<>(List.of(record(RECORD_ID, "Cipla")))); + + assertSuccessContaining(controller.getManufacturer("{\"providerServiceMapID\":4001}"), "Cipla"); + } + + @Test + @DisplayName("getManufacturer should answer an error envelope when the lookup fails") + void get_shouldAnswerErrorEnvelopeOnFailure() { + when(manufacturerInter.createManufacturer(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getManufacturer("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("editManufacturer should copy the edits onto the stored record") + void edit_shouldCopyEdits() { + M_Manufacturer stored = record(RECORD_ID, "Cipla"); + when(manufacturerInter.editManufacturer(RECORD_ID)).thenReturn(stored); + when(manufacturerInter.saveEditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editManufacturer("{\"manufacturerID\":11,\"manufacturerDesc\":\"Generic maker\",\"contactPerson\":\"Asha\",\"modifiedBy\":\"admin\"}"), "Cipla"); + assertEquals("Generic maker", stored.getManufacturerDesc()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("editManufacturer should answer an error envelope for a record that does not exist") + void edit_shouldAnswerErrorEnvelopeForUnknownRecord() { + when(manufacturerInter.editManufacturer(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.editManufacturer("{\"manufacturerID\":11,\"manufacturerDesc\":\"Generic maker\",\"contactPerson\":\"Asha\",\"modifiedBy\":\"admin\"}"))); + } + + @Test + @DisplayName("deleteManufacturer should mark the record deleted") + void delete_shouldMarkRecordDeleted() { + M_Manufacturer stored = record(RECORD_ID, "Cipla"); + when(manufacturerInter.editManufacturer(RECORD_ID)).thenReturn(stored); + when(manufacturerInter.saveEditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteManufacturer("{\"manufacturerID\":11,\"deleted\":true}"), "Cipla"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteManufacturer should answer an error envelope for a record that does not exist") + void delete_shouldAnswerErrorEnvelopeForUnknownRecord() { + when(manufacturerInter.editManufacturer(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.deleteManufacturer("{\"manufacturerID\":11,\"deleted\":true}"))); + } + + @Test + @DisplayName("checkManufacturerCode should report whether the code is already taken") + void check_shouldReportWhetherCodeIsTaken() { + when(manufacturerInter.checkManufacturerCode(any())).thenReturn(Boolean.TRUE); + + assertSuccessContaining(controller.checkManufacturerCode("{\"manufacturerCode\":\"C-1\"}"), "true"); + } + + @Test + @DisplayName("checkManufacturerCode should answer an error envelope when the check fails") + void check_shouldAnswerErrorEnvelopeOnFailure() { + when(manufacturerInter.checkManufacturerCode(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.checkManufacturerCode("{\"manufacturerCode\":\"C-1\"}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/nodalConfig/NodalConfigControllerTest.java b/src/test/java/com/iemr/admin/controller/nodalConfig/NodalConfigControllerTest.java new file mode 100644 index 0000000..e5129d3 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/nodalConfig/NodalConfigControllerTest.java @@ -0,0 +1,147 @@ +/* +* 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.admin.controller.nodalConfig; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.mock.web.MockHttpServletRequest; + +import com.iemr.admin.model.emailconfig.NodalEmailRequest; +import com.iemr.admin.model.emailconfig.NodalEmailResponse; +import com.iemr.admin.model.emailconfig.CreateNodalEmailRequestModel; +import com.iemr.admin.model.emailconfig.UpdateNodalEmailRequest; +import com.iemr.admin.service.nodalemailconfig.NodalConfigService; +import com.iemr.admin.utils.mapper.OutputMapper; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyList; +import static org.mockito.Mockito.when; + +/** + * The nodal config screen keeps the nodal officer mailboxes a complaint is + * escalated to. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("NodalConfigController Test Suite") +class NodalConfigControllerTest { + + @Mock + private NodalConfigService nodalConfigService; + + @InjectMocks + private NodalConfigController controller; + + private final MockHttpServletRequest servletRequest = new MockHttpServletRequest(); + + @BeforeEach + @DisplayName("Prime the shared output builder the screens publish through") + void setUp() { + new OutputMapper(); + } + + private static NodalEmailResponse mailbox() { + NodalEmailResponse mailbox = new NodalEmailResponse(); + mailbox.setEmailID("nodal.bengaluru@example.org"); + return mailbox; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + @Test + @DisplayName("saveConfig should answer the mailboxes it recorded") + void save_shouldAnswerRecordedMailboxes() { + when(nodalConfigService.saveNodalEmailConfigs(anyList())).thenReturn(List.of(mailbox())); + + String response = controller.saveConfig(List.of(new CreateNodalEmailRequestModel()), servletRequest); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("nodal.bengaluru@example.org"), response); + } + + @Test + @DisplayName("saveConfig should report the failure when the mailbox cannot be recorded") + void save_shouldReportStorageFailure() { + when(nodalConfigService.saveNodalEmailConfigs(anyList())).thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.saveConfig(new ArrayList<>(), servletRequest))); + } + + @Test + @DisplayName("getEmailConfigs should answer the mailboxes matching what the caller narrowed by") + void get_shouldAnswerMatchingMailboxes() { + when(nodalConfigService.getAllNodalEmailConfigs(any(NodalEmailRequest.class))).thenReturn(List.of(mailbox())); + + String response = controller.getNodalEmailConfigs(new NodalEmailRequest(), servletRequest); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("nodal.bengaluru@example.org"), response); + } + + @Test + @DisplayName("getEmailConfigs should report the failure when the mailboxes cannot be answered") + void get_shouldReportLookupFailure() { + when(nodalConfigService.getAllNodalEmailConfigs(any(NodalEmailRequest.class))) + .thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getNodalEmailConfigs(new NodalEmailRequest(), servletRequest))); + } + + @Test + @DisplayName("updateEmailConfig should answer the mailbox as it stands after the change") + void update_shouldAnswerChangedMailbox() { + when(nodalConfigService.updateNodalEmailConfigs(any(UpdateNodalEmailRequest.class))).thenReturn(mailbox()); + + String response = controller.updateNodalEmailConfig(new UpdateNodalEmailRequest(), servletRequest); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("nodal.bengaluru@example.org"), response); + } + + @Test + @DisplayName("updateEmailConfig should report the failure when the change cannot be recorded") + void update_shouldReportStorageFailure() { + when(nodalConfigService.updateNodalEmailConfigs(any(UpdateNodalEmailRequest.class))) + .thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.updateNodalEmailConfig(new UpdateNodalEmailRequest(), servletRequest))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/parkingPlace/ParkingPlaceControllerTest.java b/src/test/java/com/iemr/admin/controller/parkingPlace/ParkingPlaceControllerTest.java new file mode 100644 index 0000000..bfa0d24 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/parkingPlace/ParkingPlaceControllerTest.java @@ -0,0 +1,260 @@ +/* +* 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.admin.controller.parkingPlace; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.parkingPlace.M_Parkingplace; +import com.iemr.admin.service.parkingPlace.ParkingPlaceServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The parking place screens create, retire and re-address the places a van is + * stationed at, and answer them back filtered by state, provider or zone. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ParkingPlaceController Test Suite") +class ParkingPlaceControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer PARKING_PLACE_ID = 31; + + @Mock + private ParkingPlaceServiceImpl parkingPlaceServiceImpl; + + @InjectMocks + private ParkingPlaceController controller; + + private static M_Parkingplace place() { + M_Parkingplace place = new M_Parkingplace(); + place.setParkingPlaceID(PARKING_PLACE_ID); + place.setParkingPlaceName("Hosur parking"); + place.setParkingPlaceDesc("Near the bus stand"); + place.setProviderServiceMapID(PSM_ID); + return place; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + private static void assertFailure(String response, int expectedCode) { + assertEquals(expectedCode, statusCodeOf(response), response); + } + + @Test + @DisplayName("saveParkingPlace should answer the parking places it created") + void saveParkingPlace_shouldAnswerCreatedPlaces() throws Exception { + when(parkingPlaceServiceImpl.saveParkingPlace(anyList())) + .thenReturn(new ArrayList<>(List.of(place()))); + + String response = controller.saveParkingPlace( + "{\"parkingPlaces\":[{\"parkingPlaceName\":\"Hosur parking\",\"providerServiceMapID\":4001}]}"); + + assertSuccessContaining(response, "Hosur parking"); + } + + @Test + @DisplayName("saveParkingPlace should report the failure when the parking place cannot be stored") + void saveParkingPlace_shouldReportStorageFailure() throws Exception { + when(parkingPlaceServiceImpl.saveParkingPlace(anyList())) + .thenThrow(new RuntimeException("duplicate parking place")); + + String response = controller.saveParkingPlace("{\"parkingPlaces\":[{\"parkingPlaceName\":\"Hosur\"}]}"); + + assertFailure(response, OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("getParkingPlaces should answer the parking places of the state and district asked for") + void getParkingPlaces_shouldAnswerPlacesOfStateAndDistrict() throws Exception { + when(parkingPlaceServiceImpl.getAvailableParkingPlaces(29, 301, 5)) + .thenReturn(new ArrayList<>(List.of(place()))); + + String response = controller + .getParkingPlaces("{\"stateID\":29,\"districtID\":301,\"serviceProviderID\":5}"); + + assertSuccessContaining(response, "Hosur parking"); + } + + @Test + @DisplayName("getParkingPlaces should report the failure when the lookup cannot be answered") + void getParkingPlaces_shouldReportLookupFailure() throws Exception { + when(parkingPlaceServiceImpl.getAvailableParkingPlaces(any(), any(), any())) + .thenThrow(new RuntimeException("query timed out")); + + assertFailure(controller.getParkingPlaces("{\"stateID\":29}"), OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("deleteParkingPlace should confirm the retirement it recorded") + void deleteParkingPlace_shouldConfirmRetirement() throws Exception { + when(parkingPlaceServiceImpl.updateParkingPlaceStatus(any(M_Parkingplace.class))).thenReturn(1); + + String response = controller + .deleteParkingPlace("{\"parkingPlaceID\":31,\"deleted\":true,\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "status updated successfully"); + } + + @Test + @DisplayName("deleteParkingPlace should say so when no parking place was retired") + void deleteParkingPlace_shouldSaySoWhenNothingRetired() throws Exception { + when(parkingPlaceServiceImpl.updateParkingPlaceStatus(any(M_Parkingplace.class))).thenReturn(0); + + String response = controller.deleteParkingPlace("{\"parkingPlaceID\":-1,\"deleted\":true}"); + + assertSuccessContaining(response, "Failed to update the status"); + } + + @Test + @DisplayName("deleteParkingPlace should report the failure when the retirement cannot be recorded") + void deleteParkingPlace_shouldReportRetirementFailure() throws Exception { + when(parkingPlaceServiceImpl.updateParkingPlaceStatus(any(M_Parkingplace.class))) + .thenThrow(new RuntimeException("row is locked")); + + assertFailure(controller.deleteParkingPlace("{\"parkingPlaceID\":31}"), OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("updateParkingPlaceDetails should answer the re-addressed parking place") + void updateParkingPlaceDetails_shouldAnswerReaddressedPlace() throws Exception { + M_Parkingplace stored = place(); + when(parkingPlaceServiceImpl.getParkingPlaceByID(PARKING_PLACE_ID)).thenReturn(stored); + when(parkingPlaceServiceImpl.updateParkingPlaceData(stored)).thenReturn(stored); + + String response = controller.updateParkingPlaceDetails( + "{\"parkingPlaceID\":31,\"parkingPlaceName\":\"Hosur parking\",\"areaHQAddress\":\"Hosur Road\"," + + "\"stateID\":29,\"districtID\":301,\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "Hosur parking"); + assertEquals("Hosur Road", stored.getAreaHQAddress()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("updateParkingPlaceDetails should report the failure when the parking place is unknown") + void updateParkingPlaceDetails_shouldReportUnknownPlace() throws Exception { + when(parkingPlaceServiceImpl.getParkingPlaceByID(anyInt())).thenReturn(null); + + assertFailure(controller.updateParkingPlaceDetails("{\"parkingPlaceID\":-1}"), + OutputResponse.CODE_EXCEPTION); + } + + @Test + @DisplayName("getParkingPlacesProviderserviceMap should answer the parking places of the provider asked for") + void getParkingPlacesProviderserviceMap_shouldAnswerPlacesOfProvider() throws Exception { + when(parkingPlaceServiceImpl.getParkingPlaces(PSM_ID)).thenReturn(List.of(place())); + + assertSuccessContaining(controller.getParkingPlacesProviderserviceMap("{\"providerServiceMapID\":4001}"), + "Hosur parking"); + } + + @Test + @DisplayName("getParkingPlacesProviderserviceMap should report the failure when the lookup cannot be answered") + void getParkingPlacesProviderserviceMap_shouldReportLookupFailure() throws Exception { + when(parkingPlaceServiceImpl.getParkingPlaces(any())) + .thenThrow(new RuntimeException("connection reset")); + + assertFailure(controller.getParkingPlacesProviderserviceMap("{\"providerServiceMapID\":4001}"), + OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("getSubDistrictDetailsByParkingPlaceID should answer the taluks the parking place covers") + void getSubDistrictDetails_shouldAnswerCoveredTaluks() throws Exception { + when(parkingPlaceServiceImpl.getSubDistrict(PARKING_PLACE_ID)) + .thenReturn(List.of(new M_Parkingplace(PARKING_PLACE_ID, 3011, "Anekal"))); + + assertSuccessContaining(controller.getSubDistrictDetailsByParkingPlaceID("{\"parkingPlaceID\":31}"), + "Anekal"); + } + + @Test + @DisplayName("getSubDistrictDetailsByParkingPlaceID should report the failure when the lookup cannot be answered") + void getSubDistrictDetails_shouldReportLookupFailure() throws Exception { + when(parkingPlaceServiceImpl.getSubDistrict(any())).thenThrow(new RuntimeException("query timed out")); + + assertFailure(controller.getSubDistrictDetailsByParkingPlaceID("{\"parkingPlaceID\":31}"), + OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("getparkingPlacesbyzoneid should answer the parking places of the zone asked for") + void getparkingPlacesbyzoneid_shouldAnswerPlacesOfZone() throws Exception { + when(parkingPlaceServiceImpl.getAvailableParkingPlacesbyZoneID(9, PSM_ID)) + .thenReturn(new ArrayList<>(List.of(place()))); + + assertSuccessContaining( + controller.getparkingPlacesbyzoneid("{\"zoneID\":9,\"providerServiceMapID\":4001}"), + "Hosur parking"); + } + + @Test + @DisplayName("getparkingPlacesbyzoneid should report the failure when the lookup cannot be answered") + void getparkingPlacesbyzoneid_shouldReportLookupFailure() throws Exception { + when(parkingPlaceServiceImpl.getAvailableParkingPlacesbyZoneID(any(), any())) + .thenThrow(new RuntimeException("connection reset")); + + assertFailure(controller.getparkingPlacesbyzoneid("{\"zoneID\":9}"), OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("a malformed request body should be rejected rather than reach the service") + void malformedBody_shouldBeRejected() throws Exception { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(runMalformed()), "malformed JSON must be refused"); + } + + private String runMalformed() throws Exception { + try { + return controller.getParkingPlaces("{not json"); + } catch (Exception e) { + OutputResponse output = new OutputResponse(); + output.setError(e); + return output.toString(); + } + } +} diff --git a/src/test/java/com/iemr/admin/controller/parkingPlace/ParkingPlaceTalukMappingControllerTest.java b/src/test/java/com/iemr/admin/controller/parkingPlace/ParkingPlaceTalukMappingControllerTest.java new file mode 100644 index 0000000..30821f2 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/parkingPlace/ParkingPlaceTalukMappingControllerTest.java @@ -0,0 +1,261 @@ +/* +* 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.admin.controller.parkingPlace; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.locationmaster.DistrictBlock; +import com.iemr.admin.data.parkingPlace.ParkingplaceTalukMapping; +import com.iemr.admin.data.parkingPlace.ParkingplaceTalukMappingTO; +import com.iemr.admin.service.parkingPlace.ParkingPlaceTalukMappingServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The taluk mapping screen records which taluks a parking place covers, and + * offers the district's remaining taluks as the candidates for a new mapping. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ParkingPlaceTalukMappingController Test Suite") +class ParkingPlaceTalukMappingControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer PARKING_PLACE_ID = 31; + private static final Integer MAP_ID = 7001; + private static final Integer DISTRICT_ID = 301; + + @Mock + private ParkingPlaceTalukMappingServiceImpl parkingPlaceTalukMappingServiceImpl; + + @InjectMocks + private ParkingPlaceTalukMappingController controller; + + private static ParkingplaceTalukMapping mapping() { + ParkingplaceTalukMapping mapping = new ParkingplaceTalukMapping(); + mapping.setPpSubDistrictMapID(MAP_ID); + mapping.setParkingPlaceID(PARKING_PLACE_ID); + mapping.setDistrictID(DISTRICT_ID); + mapping.setDistrictBlockID(3011); + mapping.setProviderServiceMapID(PSM_ID); + mapping.setCreatedBy("admin"); + return mapping; + } + + private static ParkingplaceTalukMappingTO published() { + ParkingplaceTalukMappingTO to = new ParkingplaceTalukMappingTO(); + to.setPpSubDistrictMapID(MAP_ID); + to.setDistrictBlockName("Anekal"); + return to; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + private static void assertFailure(String response, int expectedCode) { + assertEquals(expectedCode, statusCodeOf(response), response); + } + + @Test + @DisplayName("parkingPlacesTalukMapping should answer the mappings it created") + void create_shouldAnswerCreatedMappings() { + when(parkingPlaceTalukMappingServiceImpl.saveParkingPlaceTalukMapping(anyList())) + .thenReturn(new ArrayList<>(List.of(mapping()))); + + assertSuccessContaining(controller.parkingPlacesTalukMapping(List.of(mapping())), "7001"); + } + + @Test + @DisplayName("parkingPlacesTalukMapping should report the failure when the mapping cannot be stored") + void create_shouldReportStorageFailure() { + when(parkingPlaceTalukMappingServiceImpl.saveParkingPlaceTalukMapping(anyList())) + .thenThrow(new RuntimeException("taluk already mapped")); + + assertFailure(controller.parkingPlacesTalukMapping(List.of(mapping())), OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("updateparkingPlacesTalukMapping should answer the mapping it moved") + void update_shouldAnswerMovedMapping() { + ParkingplaceTalukMapping stored = mapping(); + when(parkingPlaceTalukMappingServiceImpl.findbyID(MAP_ID)).thenReturn(stored); + when(parkingPlaceTalukMappingServiceImpl.updateParkingPlaceTalukMapping(stored)).thenReturn(stored); + + assertSuccessContaining(controller.updateparkingPlacesTalukMapping(mapping()), "7001"); + assertEquals("admin", stored.getModifiedBy(), "the caller must be recorded as the one who moved it"); + } + + @Test + @DisplayName("updateparkingPlacesTalukMapping should leave the request alone when no mapping is named") + void update_shouldLeaveRequestAloneWithoutMapId() { + ParkingplaceTalukMapping request = mapping(); + request.setPpSubDistrictMapID(null); + + assertSuccessContaining(controller.updateparkingPlacesTalukMapping(request), "31"); + verify(parkingPlaceTalukMappingServiceImpl, never()).findbyID(anyInt()); + } + + @Test + @DisplayName("updateparkingPlacesTalukMapping should report the failure when the mapping is unknown") + void update_shouldReportUnknownMapping() { + when(parkingPlaceTalukMappingServiceImpl.findbyID(MAP_ID)).thenReturn(null); + + assertFailure(controller.updateparkingPlacesTalukMapping(mapping()), OutputResponse.CODE_EXCEPTION); + } + + @Test + @DisplayName("getparkingPlacesTalukMapping should answer the mapping asked for") + void getById_shouldAnswerNamedMapping() { + when(parkingPlaceTalukMappingServiceImpl.findbyID(MAP_ID)).thenReturn(mapping()); + + assertSuccessContaining(controller.getparkingPlacesTalukMapping(mapping()), "7001"); + } + + @Test + @DisplayName("getparkingPlacesTalukMapping should echo the request when no mapping is named") + void getById_shouldEchoRequestWithoutMapId() { + ParkingplaceTalukMapping request = mapping(); + request.setPpSubDistrictMapID(null); + + assertSuccessContaining(controller.getparkingPlacesTalukMapping(request), "31"); + verify(parkingPlaceTalukMappingServiceImpl, never()).findbyID(anyInt()); + } + + @Test + @DisplayName("getparkingPlacesTalukMapping should report the failure when the lookup cannot be answered") + void getById_shouldReportLookupFailure() { + when(parkingPlaceTalukMappingServiceImpl.findbyID(MAP_ID)) + .thenThrow(new RuntimeException("query timed out")); + + assertFailure(controller.getparkingPlacesTalukMapping(mapping()), OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("getallparkingPlacesTalukMapping should answer every taluk the parking place covers") + void getAll_shouldAnswerCoveredTaluks() { + when(parkingPlaceTalukMappingServiceImpl.findbyProviderservicemapid(any())) + .thenReturn(List.of(published())); + + assertSuccessContaining(controller.getallparkingPlacesTalukMapping(mapping()), "Anekal"); + } + + @Test + @DisplayName("getallparkingPlacesTalukMapping should report the failure when the lookup cannot be answered") + void getAll_shouldReportLookupFailure() { + when(parkingPlaceTalukMappingServiceImpl.findbyProviderservicemapid(any())) + .thenThrow(new RuntimeException("connection reset")); + + assertFailure(controller.getallparkingPlacesTalukMapping(mapping()), OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("getafilterparkingPlacesTalukMapping should narrow the mappings to the district asked for") + void getFiltered_shouldNarrowToDistrict() { + when(parkingPlaceTalukMappingServiceImpl.findbyParkingplaceAndDistrictID(any())) + .thenReturn(List.of(published())); + + assertSuccessContaining(controller.getafilterparkingPlacesTalukMapping(mapping()), "Anekal"); + } + + @Test + @DisplayName("getafilterparkingPlacesTalukMapping should report the failure when the lookup cannot be answered") + void getFiltered_shouldReportLookupFailure() { + when(parkingPlaceTalukMappingServiceImpl.findbyParkingplaceAndDistrictID(any())) + .thenThrow(new RuntimeException("query timed out")); + + assertFailure(controller.getafilterparkingPlacesTalukMapping(mapping()), OutputResponse.GENERIC_FAILURE); + } + + @Test + @DisplayName("activateparkingPlacesTalukMapping should answer the mapping whose status it changed") + void activate_shouldAnswerChangedMapping() { + ParkingplaceTalukMapping stored = mapping(); + ParkingplaceTalukMapping request = mapping(); + request.setDeleted(Boolean.TRUE); + when(parkingPlaceTalukMappingServiceImpl.findbyID(MAP_ID)).thenReturn(stored); + when(parkingPlaceTalukMappingServiceImpl.updateParkingPlaceTalukMapping(stored)).thenReturn(stored); + + assertSuccessContaining(controller.activateparkingPlacesTalukMapping(request), "7001"); + assertEquals(Boolean.TRUE, stored.getDeleted(), "the mapping must take the requested status"); + } + + @Test + @DisplayName("activateparkingPlacesTalukMapping should leave the request alone when no mapping is named") + void activate_shouldLeaveRequestAloneWithoutMapId() { + ParkingplaceTalukMapping request = mapping(); + request.setPpSubDistrictMapID(null); + + assertSuccessContaining(controller.activateparkingPlacesTalukMapping(request), "31"); + verify(parkingPlaceTalukMappingServiceImpl, never()).findbyID(anyInt()); + } + + @Test + @DisplayName("activateparkingPlacesTalukMapping should report the failure when the mapping is unknown") + void activate_shouldReportUnknownMapping() { + when(parkingPlaceTalukMappingServiceImpl.findbyID(MAP_ID)).thenReturn(null); + + assertFailure(controller.activateparkingPlacesTalukMapping(mapping()), OutputResponse.CODE_EXCEPTION); + } + + @Test + @DisplayName("getunmappedtaluk should offer the taluks that are still free") + void getunmappedtaluk_shouldOfferFreeTaluks() { + when(parkingPlaceTalukMappingServiceImpl.getunmappedtaluk(DISTRICT_ID, PSM_ID)) + .thenReturn(List.of(new DistrictBlock(3012, "Hoskote"))); + + assertSuccessContaining(controller.getunmappedtaluk(mapping()), "Hoskote"); + } + + @Test + @DisplayName("getunmappedtaluk should report the failure when the candidates cannot be worked out") + void getunmappedtaluk_shouldReportLookupFailure() { + when(parkingPlaceTalukMappingServiceImpl.getunmappedtaluk(any(), any())) + .thenThrow(new RuntimeException("query timed out")); + + assertFailure(controller.getunmappedtaluk(mapping()), OutputResponse.GENERIC_FAILURE); + } +} diff --git a/src/test/java/com/iemr/admin/controller/pharmacologicalcategory/PharmacologicalCategoryControllerTest.java b/src/test/java/com/iemr/admin/controller/pharmacologicalcategory/PharmacologicalCategoryControllerTest.java new file mode 100644 index 0000000..7ceb4be --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/pharmacologicalcategory/PharmacologicalCategoryControllerTest.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.admin.controller.pharmacologicalcategory; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.pharmacologicalcategory.M_Pharmacologicalcategory; +import com.iemr.admin.service.pharmacologicalcategory.PharmacologicalcategoryInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** The pharmacological category master endpoints keep the pharmacological category catalogue an inventory operator picks from. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("PharmacologicalCategoryController Test Suite") +class PharmacologicalCategoryControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer RECORD_ID = 11; + + @Mock + private PharmacologicalcategoryInter pharmacologicalcategoryInter; + + @InjectMocks + private PharmacologicalCategoryController controller; + + private static M_Pharmacologicalcategory record(Integer id, String name) { + M_Pharmacologicalcategory record = new M_Pharmacologicalcategory(); + record.setPharmacologyCategoryID(id); + record.setPharmCategoryName(name); + return record; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("createPharmacologicalcategory should answer the records the service stored") + void create_shouldAnswerStoredRecords() { + when(pharmacologicalcategoryInter.createPharmacologicalcategory(anyList())).thenReturn(new ArrayList<>(List.of(record(RECORD_ID, "Analgesic")))); + + assertSuccessContaining(controller.createPharmacologicalcategory("[{\"pharmCategoryCode\":\"C-1\"}]"), "Analgesic"); + } + + @Test + @DisplayName("createPharmacologicalcategory should answer an error envelope when the store fails") + void create_shouldAnswerErrorEnvelopeOnFailure() { + when(pharmacologicalcategoryInter.createPharmacologicalcategory(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createPharmacologicalcategory("[{}]")); + } + + @Test + @DisplayName("getPharmacologicalcategory should answer the records of the provider") + void get_shouldAnswerProviderRecords() { + when(pharmacologicalcategoryInter.getPharmacologicalcategory(PSM_ID)).thenReturn(new ArrayList<>(List.of(record(RECORD_ID, "Analgesic")))); + + assertSuccessContaining(controller.getPharmacologicalcategory("{\"providerServiceMapID\":4001}"), "Analgesic"); + } + + @Test + @DisplayName("getPharmacologicalcategory should answer an error envelope when the lookup fails") + void get_shouldAnswerErrorEnvelopeOnFailure() { + when(pharmacologicalcategoryInter.getPharmacologicalcategory(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getPharmacologicalcategory("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("editPharmacologicalcategory should copy the edits onto the stored record") + void edit_shouldCopyEdits() { + M_Pharmacologicalcategory stored = record(RECORD_ID, "Analgesic"); + when(pharmacologicalcategoryInter.editPharmacologicalcategory(RECORD_ID)).thenReturn(stored); + when(pharmacologicalcategoryInter.saveEditedPharData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editPharmacologicalcategory("{\"pharmacologyCategoryID\":11,\"pharmCategoryDesc\":\"Pain relief\",\"modifiedBy\":\"admin\"}"), "Analgesic"); + assertEquals("Pain relief", stored.getPharmCategoryDesc()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("editPharmacologicalcategory should answer an error envelope for a record that does not exist") + void edit_shouldAnswerErrorEnvelopeForUnknownRecord() { + when(pharmacologicalcategoryInter.editPharmacologicalcategory(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.editPharmacologicalcategory("{\"pharmacologyCategoryID\":11,\"pharmCategoryDesc\":\"Pain relief\",\"modifiedBy\":\"admin\"}"))); + } + + @Test + @DisplayName("deletePharmacologicalcategory should mark the record deleted") + void delete_shouldMarkRecordDeleted() { + M_Pharmacologicalcategory stored = record(RECORD_ID, "Analgesic"); + when(pharmacologicalcategoryInter.editPharmacologicalcategory(RECORD_ID)).thenReturn(stored); + when(pharmacologicalcategoryInter.saveEditedPharData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deletePharmacologicalcategory("{\"pharmacologyCategoryID\":11,\"deleted\":true}"), "Analgesic"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deletePharmacologicalcategory should answer an error envelope for a record that does not exist") + void delete_shouldAnswerErrorEnvelopeForUnknownRecord() { + when(pharmacologicalcategoryInter.editPharmacologicalcategory(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.deletePharmacologicalcategory("{\"pharmacologyCategoryID\":11,\"deleted\":true}"))); + } + + @Test + @DisplayName("checkPharmacologicalcategoryCode should report whether the code is already taken") + void check_shouldReportWhetherCodeIsTaken() { + when(pharmacologicalcategoryInter.checkPharmacologicalcategoryCode(any())).thenReturn(Boolean.TRUE); + + assertSuccessContaining(controller.checkPharmacologicalcategoryCode("{\"pharmCategoryCode\":\"C-1\"}"), "true"); + } + + @Test + @DisplayName("checkPharmacologicalcategoryCode should answer an error envelope when the check fails") + void check_shouldAnswerErrorEnvelopeOnFailure() { + when(pharmacologicalcategoryInter.checkPharmacologicalcategoryCode(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.checkPharmacologicalcategoryCode("{\"pharmCategoryCode\":\"C-1\"}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardCallTypeControllerTest.java b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardCallTypeControllerTest.java new file mode 100644 index 0000000..67077e1 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardCallTypeControllerTest.java @@ -0,0 +1,288 @@ +/* +* 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.admin.controller.provideronboard; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.iemr.admin.data.provideronboard.M_Calltype; +import com.iemr.admin.data.provideronboard.M_Subservice; +import com.iemr.admin.data.provideronboard.M_SubservicemasterPA; + +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.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The call type and sub-service endpoints define the call taxonomy a provider's + * agents work with. + */ +@DisplayName("ProviderOnBoardController call type Test Suite") +class ProviderOnBoardCallTypeControllerTest extends ProviderOnBoardFixture { + + private static M_Calltype callType(Integer id, String name) { + M_Calltype callType = new M_Calltype(); + callType.setCallTypeID(id); + callType.setCallType(name); + return callType; + } + + private static M_Subservice subService(Integer id, String name) { + M_Subservice subService = new M_Subservice(); + subService.setSubServiceID(id); + subService.setSubServiceName(name); + return subService; + } + + @Test + @DisplayName("saveCallTypeData should flatten the nested request into one call type per description") + void saveCallTypeData_shouldFlattenNestedRequest() { + ArrayList stored = new ArrayList<>(List.of(callType(51, "Medical Advice"))); + when(calltypeinter.saveCallList(anyList())).thenReturn(stored); + + String request = "[{\"callGroupType\":\"Inbound\",\"createdBy\":\"admin\",\"callType1\":" + + "[{\"calltype\":\"Medical Advice\",\"providerServiceMapID\":4001," + + "\"callTypeDesc1\":[\"General\",\"Urgent\"],\"fitToBlock1\":[\"true\",\"false\"]," + + "\"fitForFollowup1\":[true,false],\"isInbound1\":[true,true]," + + "\"isOutbound1\":[false,false]}]}]"; + + assertSuccessContaining(controller.saveCallTypeData(request), "Medical Advice"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(calltypeinter).saveCallList(captor.capture()); + List saved = captor.getValue(); + assertEquals(2, saved.size(), "each description must become its own call type"); + assertEquals("Inbound", saved.get(0).getCallGroupType()); + assertEquals("General", saved.get(0).getCallTypeDesc()); + assertTrue(saved.get(0).getFitToBlock()); + assertEquals(4001, saved.get(0).getProviderServiceMapID()); + } + + @Test + @DisplayName("saveCallTypeData should answer an error envelope for a request it cannot read") + void saveCallTypeData_shouldAnswerErrorEnvelopeForUnreadableRequest() { + assertCodeException(controller.saveCallTypeData("[{\"callGroupType\":\"Inbound\"}]")); + } + + @Test + @DisplayName("createCalltypeData should answer the call types the service stored") + void createCalltypeData_shouldAnswerStoredCallTypes() { + ArrayList stored = new ArrayList<>(List.of(callType(51, "Medical Advice"))); + when(calltypeinter.createCalltype(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.createCalltypeData("[{\"callType\":\"Medical Advice\"}]"), + "Medical Advice"); + } + + @Test + @DisplayName("createCalltypeData should answer an error envelope when the store fails") + void createCalltypeData_shouldAnswerErrorEnvelopeOnFailure() { + when(calltypeinter.createCalltype(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createCalltypeData("[{\"callType\":\"Medical Advice\"}]")); + } + + @Test + @DisplayName("getCallTypeData should answer the call types configured for the mapping") + void getCallTypeData_shouldAnswerConfiguredCallTypes() { + ArrayList stored = new ArrayList<>(List.of(callType(51, "Medical Advice"))); + when(calltypeinter.getCalltypeData(4001)).thenReturn(stored); + + assertSuccessContaining(controller.getCallTypeData("{\"providerServiceMapID\":4001}"), "Medical Advice"); + } + + @Test + @DisplayName("getCallTypeData should answer an error envelope when the lookup fails") + void getCallTypeData_shouldAnswerErrorEnvelopeOnFailure() { + when(calltypeinter.getCalltypeData(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getCallTypeData("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("updateCallTypeData should copy the edited fields onto the stored call type") + void updateCallTypeData_shouldCopyEditedFields() { + M_Calltype stored = callType(51, "old name"); + when(calltypeinter.updateCallType(51)).thenReturn(stored); + when(calltypeinter.saveupdatedData(stored)).thenReturn(stored); + + String response = controller.updateCallTypeData("{\"callTypeID\":51,\"callType\":\"Medical Advice\"," + + "\"callGroupType\":\"Inbound\",\"callTypeDesc\":\"General\",\"providerServiceMapID\":4001," + + "\"fitToBlock\":true,\"fitForFollowup\":false,\"isInbound\":true,\"isOutbound\":false," + + "\"processed\":\"N\",\"maxRedial\":3}"); + + assertSuccessContaining(response, "Medical Advice"); + assertEquals("Inbound", stored.getCallGroupType()); + assertEquals(3, stored.getMaxRedial()); + } + + @Test + @DisplayName("updateCallTypeData should answer an error envelope for a call type that does not exist") + void updateCallTypeData_shouldAnswerErrorEnvelopeForUnknownCallType() { + when(calltypeinter.updateCallType(51)).thenReturn(null); + + assertCodeException(controller.updateCallTypeData("{\"callTypeID\":51}")); + } + + @Test + @DisplayName("deleteCallType should mark the call type deleted and answer what was saved") + void deleteCallType_shouldMarkCallTypeDeleted() { + M_Calltype stored = callType(51, "Medical Advice"); + when(calltypeinter.updateCallType(51)).thenReturn(stored); + when(calltypeinter.saveupdatedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteCallType("{\"callTypeID\":51,\"deleted\":true}"), "Medical Advice"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteCallType should answer an error envelope for a call type that does not exist") + void deleteCallType_shouldAnswerErrorEnvelopeForUnknownCallType() { + when(calltypeinter.updateCallType(51)).thenReturn(null); + + assertCodeException(controller.deleteCallType("{\"callTypeID\":51,\"deleted\":true}")); + } + + @Test + @DisplayName("saveSubServiceData should flatten the nested request onto the provider mapping") + void saveSubServiceData_shouldFlattenNestedRequest() { + ArrayList stored = new ArrayList<>(List.of(subService(61, "Counselling"))); + when(subServiceInter.saveSubList(anyList())).thenReturn(stored); + + String request = "[{\"providerServiceMapID\":4001,\"createdBy\":\"admin\",\"subServiceDetails\":" + + "[{\"subServiceName\":\"Counselling\",\"subServiceDesc\":\"Mental health\"}]}]"; + + assertSuccessContaining(controller.saveSubServiceData(request), "Counselling"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(subServiceInter).saveSubList(captor.capture()); + assertEquals(1, captor.getValue().size()); + assertEquals(4001, captor.getValue().get(0).getProviderServiceMapID()); + assertEquals("admin", captor.getValue().get(0).getCreatedBy()); + } + + @Test + @DisplayName("saveSubServiceData should answer an error envelope for a request it cannot read") + void saveSubServiceData_shouldAnswerErrorEnvelopeForUnreadableRequest() { + assertCodeException(controller.saveSubServiceData("[{\"providerServiceMapID\":4001}]")); + } + + @Test + @DisplayName("FindSubSeriveNameByMapId should answer the master sub-services for the service") + void findSubSeriveNameByMapId_shouldAnswerMasterSubServices() { + M_SubservicemasterPA master = new M_SubservicemasterPA(); + master.setSubServiceMasterID(71); + master.setSubServiceName("Counselling"); + ArrayList stored = new ArrayList<>(List.of(master)); + when(subServiceInter.getServiceNameByServiceID(3)).thenReturn(stored); + + assertSuccessContaining(controller.FindSubSeriveNameByMapId("{\"serviceID\":3}"), "Counselling"); + } + + @Test + @DisplayName("FindSubSeriveNameByMapId should answer an error envelope when the lookup fails") + void findSubSeriveNameByMapId_shouldAnswerErrorEnvelopeOnFailure() { + when(subServiceInter.getServiceNameByServiceID(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.FindSubSeriveNameByMapId("{\"serviceID\":3}")); + } + + @Test + @DisplayName("getSubSeriveName should answer the sub-services configured for the mapping") + void getSubSeriveName_shouldAnswerConfiguredSubServices() { + ArrayList stored = new ArrayList<>(List.of(subService(61, "Counselling"))); + when(subServiceInter.getsubServiceName(4001)).thenReturn(stored); + + assertSuccessContaining(controller.getSubSeriveName("{\"providerServiceMapID\":4001}"), "Counselling"); + } + + @Test + @DisplayName("getSubSeriveName should answer an error envelope when the lookup fails") + void getSubSeriveName_shouldAnswerErrorEnvelopeOnFailure() { + when(subServiceInter.getsubServiceName(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getSubSeriveName("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("updateSubSerive should copy the edited fields onto the stored sub-service") + void updateSubSerive_shouldCopyEditedFields() { + M_Subservice stored = subService(61, "old name"); + when(subServiceInter.getsubServiceNameById(61)).thenReturn(stored); + when(subServiceInter.saveupdatedData(stored)).thenReturn(stored); + + String response = controller.updateSubSerive("{\"subServiceID\":61,\"subServiceName\":\"Counselling\"," + + "\"subServiceDesc\":\"Mental health\",\"providerServiceMapID\":4001,\"processed\":\"N\"}"); + + assertSuccessContaining(response, "Counselling"); + assertEquals("Mental health", stored.getSubServiceDesc()); + verify(subServiceInter).saveupdatedData(stored); + } + + @Test + @DisplayName("updateSubSerive should answer an error envelope for a sub-service that does not exist") + void updateSubSerive_shouldAnswerErrorEnvelopeForUnknownSubService() { + when(subServiceInter.getsubServiceNameById(61)).thenReturn(null); + + assertCodeException(controller.updateSubSerive("{\"subServiceID\":61}")); + } + + @Test + @DisplayName("deleteSubSerive should mark the sub-service deleted and answer what was saved") + void deleteSubSerive_shouldMarkSubServiceDeleted() { + M_Subservice stored = subService(61, "Counselling"); + when(subServiceInter.getsubServiceNameById(61)).thenReturn(stored); + when(subServiceInter.saveupdatedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteSubSerive("{\"subServiceID\":61,\"deleted\":true}"), "Counselling"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteSubSerive should answer an error envelope for a sub-service that does not exist") + void deleteSubSerive_shouldAnswerErrorEnvelopeForUnknownSubService() { + when(subServiceInter.getsubServiceNameById(61)).thenReturn(null); + + assertCodeException(controller.deleteSubSerive("{\"subServiceID\":61,\"deleted\":true}")); + } + + @Test + @DisplayName("saveupdatedData should be reached with the resolved record rather than the request") + void updateSubSerive_shouldSaveTheResolvedRecord() { + M_Subservice stored = subService(61, "old name"); + when(subServiceInter.getsubServiceNameById(61)).thenReturn(stored); + when(subServiceInter.saveupdatedData(any())).thenReturn(stored); + + controller.updateSubSerive("{\"subServiceID\":61,\"subServiceName\":\"Counselling\"}"); + + verify(subServiceInter).saveupdatedData(stored); + } +} diff --git a/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardCategoryControllerTest.java b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardCategoryControllerTest.java new file mode 100644 index 0000000..11e27da --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardCategoryControllerTest.java @@ -0,0 +1,688 @@ +/* +* 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.admin.controller.provideronboard; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.iemr.admin.data.provideronboard.M_Category; +import com.iemr.admin.data.provideronboard.M_Feedbacknature; +import com.iemr.admin.data.provideronboard.M_Feedbacktype; +import com.iemr.admin.data.provideronboard.M_Severity; +import com.iemr.admin.data.provideronboard.M_Subcategory; +import com.iemr.admin.data.provideronboard.V_Showsubcategory; + +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.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The category, severity and feedback endpoints define how a provider's agents + * classify a call once it has been taken. + */ +@DisplayName("ProviderOnBoardController category Test Suite") +class ProviderOnBoardCategoryControllerTest extends ProviderOnBoardFixture { + + private static final String SUBCAT_REQUEST = "{\"categoryID\":81,\"createdBy\":\"admin\",\"subcatArray\":" + + "[{\"subCategoryName\":\"Fever\",\"subCategoryDesc\":\"High temperature\"," + + "\"subCatFilePath\":\"/fever\"}]}"; + + private static M_Category category(Integer id, String name) { + M_Category category = new M_Category(); + category.setCategoryID(id); + category.setCategoryName(name); + return category; + } + + private static M_Subcategory subCategory(Integer id, String name) { + M_Subcategory subCategory = new M_Subcategory(); + subCategory.setSubCategoryID(id); + subCategory.setSubCategoryName(name); + return subCategory; + } + + @Test + @DisplayName("saveCategory should attach the sub-categories to the category the service resolves") + void saveCategory_shouldAttachSubCategoriesToResolvedCategory() { + ArrayList stored = new ArrayList<>(List.of(subCategory(91, "Fever"))); + when(categoryInter.getCategoryId(any())).thenReturn(81); + when(categoryInter.saveSubCatData(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.saveCategory(SUBCAT_REQUEST), "Fever"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(categoryInter).saveSubCatData(captor.capture()); + assertEquals(81, captor.getValue().get(0).getCategoryID()); + assertEquals("admin", captor.getValue().get(0).getCreatedBy()); + } + + @Test + @DisplayName("saveCategory should answer an error envelope when no sub-categories are named") + void saveCategory_shouldAnswerErrorEnvelopeWithoutSubCategories() { + when(categoryInter.getCategoryId(any())).thenReturn(81); + + assertCodeException(controller.saveCategory("{\"categoryID\":81}")); + } + + @Test + @DisplayName("saveCategoryUseExist should attach the sub-categories to the category the caller names") + void saveCategoryUseExist_shouldAttachSubCategoriesToNamedCategory() { + ArrayList stored = new ArrayList<>(List.of(subCategory(91, "Fever"))); + when(categoryInter.saveSubCatData(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.saveCategoryUseExist(SUBCAT_REQUEST), "Fever"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(categoryInter).saveSubCatData(captor.capture()); + assertEquals(81, captor.getValue().get(0).getCategoryID()); + } + + @Test + @DisplayName("saveCategoryUseExist should answer an error envelope when no sub-categories are named") + void saveCategoryUseExist_shouldAnswerErrorEnvelopeWithoutSubCategories() { + assertCodeException(controller.saveCategoryUseExist("{\"categoryID\":81}")); + } + + @Test + @DisplayName("getCategoryBySubServiceID should answer the categories under the sub-service") + void getCategoryBySubServiceID_shouldAnswerCategories() { + V_Showsubcategory view = new V_Showsubcategory(); + view.setSubCategoryID(91); + view.setSubCategoryName("Fever"); + ArrayList stored = new ArrayList<>(List.of(view)); + when(categoryInter.getCategoryByMapIDAndSubServiceID(4001, 61)).thenReturn(stored); + + assertSuccessContaining( + controller.getCategoryBySubServiceID("{\"providerServiceMapID\":4001,\"subServiceID\":61}"), "Fever"); + } + + @Test + @DisplayName("getCategoryBySubServiceID should answer an error envelope when the lookup fails") + void getCategoryBySubServiceID_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.getCategoryByMapIDAndSubServiceID(anyInt(), anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure( + controller.getCategoryBySubServiceID("{\"providerServiceMapID\":4001,\"subServiceID\":61}")); + } + + @Test + @DisplayName("getsubCategory should answer the sub-categories of the category") + void getsubCategory_shouldAnswerSubCategories() { + ArrayList stored = new ArrayList<>(List.of(subCategory(91, "Fever"))); + when(categoryInter.getCategory(81)).thenReturn(stored); + + assertSuccessContaining(controller.getsubCategory("{\"categoryID\":81}"), "Fever"); + } + + @Test + @DisplayName("getsubCategory should answer an error envelope when the lookup fails") + void getsubCategory_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.getCategory(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getsubCategory("{\"categoryID\":81}")); + } + + @Test + @DisplayName("getCategory should answer the categories under the sub-service and mapping") + void getCategory_shouldAnswerCategories() { + ArrayList stored = new ArrayList<>(List.of(category(81, "Medical"))); + when(categoryInter.getAllCategory(61, 4001)).thenReturn(stored); + + assertSuccessContaining(controller.getCategory("{\"subServiceID\":61,\"providerServiceMapID\":4001}"), + "Medical"); + } + + @Test + @DisplayName("getCategory should answer an error envelope when the lookup fails") + void getCategory_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.getAllCategory(anyInt(), anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getCategory("{\"subServiceID\":61,\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("updateSubCategory should copy the edited fields onto the stored sub-category") + void updateSubCategory_shouldCopyEditedFields() { + M_Subcategory stored = subCategory(91, "old name"); + when(categoryInter.getSubCategory(91)).thenReturn(stored); + when(categoryInter.updateSubCatData(stored)).thenReturn(stored); + + String response = controller.updateSubCategory("{\"subCategoryID\":91,\"categoryID\":81," + + "\"subCategoryName\":\"Fever\",\"subCategoryDesc\":\"High temperature\"," + + "\"subCatFilePath\":\"/fever\"}"); + + assertSuccessContaining(response, "Fever"); + assertEquals("High temperature", stored.getSubCategoryDesc()); + assertEquals("/fever", stored.getSubCatFilePath()); + } + + @Test + @DisplayName("updateSubCategory should answer an error envelope for a sub-category that does not exist") + void updateSubCategory_shouldAnswerErrorEnvelopeForUnknownSubCategory() { + when(categoryInter.getSubCategory(91)).thenReturn(null); + + assertCodeException(controller.updateSubCategory("{\"subCategoryID\":91}")); + } + + @Test + @DisplayName("createCategory should answer the categories the service stored") + void createCategory_shouldAnswerStoredCategories() { + ArrayList stored = new ArrayList<>(List.of(category(81, "Medical"))); + when(categoryInter.createcat(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.createCategory("[{\"categoryName\":\"Medical\"}]"), "Medical"); + } + + @Test + @DisplayName("createCategory should answer an error envelope when the store fails") + void createCategory_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.createcat(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createCategory("[{\"categoryName\":\"Medical\"}]")); + } + + @Test + @DisplayName("deleteCategory1 should mark the category deleted and answer what was saved") + void deleteCategory1_shouldMarkCategoryDeleted() { + M_Category stored = category(81, "Medical"); + when(categoryInter.getcatdatabycatId(81)).thenReturn(stored); + when(categoryInter.deletedata(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteCategory1("{\"categoryID\":81,\"deleted\":true}"), "Medical"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteCategory1 should answer an error envelope for a category that does not exist") + void deleteCategory1_shouldAnswerErrorEnvelopeForUnknownCategory() { + when(categoryInter.getcatdatabycatId(81)).thenReturn(null); + + assertCodeException(controller.deleteCategory1("{\"categoryID\":81,\"deleted\":true}")); + } + + @Test + @DisplayName("createSubCategory should answer the sub-categories the service stored") + void createSubCategory_shouldAnswerStoredSubCategories() { + ArrayList stored = new ArrayList<>(List.of(subCategory(91, "Fever"))); + when(categoryInter.createSubCategory(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.createSubCategory("[{\"subCategoryName\":\"Fever\"}]"), "Fever"); + } + + @Test + @DisplayName("createSubCategory should answer an error envelope when the store fails") + void createSubCategory_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.createSubCategory(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createSubCategory("[{\"subCategoryName\":\"Fever\"}]")); + } + + @Test + @DisplayName("deleteSubCategory should mark the sub-category deleted and answer what was saved") + void deleteSubCategory_shouldMarkSubCategoryDeleted() { + M_Subcategory stored = subCategory(91, "Fever"); + when(categoryInter.getSubCategory(91)).thenReturn(stored); + when(categoryInter.updateSubCatData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteSubCategory("{\"subCategoryID\":91,\"deleted\":true}"), "Fever"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteSubCategory should answer an error envelope for a sub-category that does not exist") + void deleteSubCategory_shouldAnswerErrorEnvelopeForUnknownSubCategory() { + when(categoryInter.getSubCategory(91)).thenReturn(null); + + assertCodeException(controller.deleteSubCategory("{\"subCategoryID\":91,\"deleted\":true}")); + } + + @Test + @DisplayName("getSubCategory should answer the sub-category view the service resolves") + void getSubCategory_shouldAnswerSubCategoryView() { + V_Showsubcategory view = new V_Showsubcategory(); + view.setSubCategoryID(91); + view.setSubCategoryName("Fever"); + ArrayList stored = new ArrayList<>(List.of(view)); + when(categoryInter.getSubCategory1(91)).thenReturn(stored); + + assertSuccessContaining(controller.getSubCategory("{\"subCategoryID\":91}"), "Fever"); + } + + @Test + @DisplayName("getSubCategory should answer an error envelope when the lookup fails") + void getSubCategory_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.getSubCategory1(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getSubCategory("{\"subCategoryID\":91}")); + } + + @Test + @DisplayName("updateCategory should copy the edited fields onto the stored category") + void updateCategory_shouldCopyEditedFields() { + M_Category stored = category(81, "old name"); + when(categoryInter.getcatdatabycatId(81)).thenReturn(stored); + when(categoryInter.deletedata(stored)).thenReturn(stored); + + String response = controller.updateCategory("{\"categoryID\":81,\"categoryName\":\"Medical\"," + + "\"categoryDesc\":\"Clinical calls\",\"modifiedBy\":\"admin\",\"s104_CS_Type\":true}"); + + assertSuccessContaining(response, "Medical"); + assertEquals("Clinical calls", stored.getCategoryDesc()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("updateCategory should answer an error envelope for a category that does not exist") + void updateCategory_shouldAnswerErrorEnvelopeForUnknownCategory() { + when(categoryInter.getcatdatabycatId(81)).thenReturn(null); + + assertCodeException(controller.updateCategory("{\"categoryID\":81}")); + } + + @Test + @DisplayName("mapCategorytoFeedbackNature should map every category the request names") + void mapCategorytoFeedbackNature_shouldMapEveryCategory() { + when(categoryInter.updateCategory(81, 21)).thenReturn(1); + when(categoryInter.updateCategory(82, 22)).thenReturn(1); + + String response = controller.mapCategorytoFeedbackNature( + "[{\"categoryID\":81,\"feedbackNatureID\":21},{\"categoryID\":82,\"feedbackNatureID\":22}]"); + + assertSuccessContaining(response, "inserted"); + verify(categoryInter).updateCategory(81, 21); + verify(categoryInter).updateCategory(82, 22); + } + + @Test + @DisplayName("mapCategorytoFeedbackNature should answer an error envelope when the mapping fails") + void mapCategorytoFeedbackNature_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.updateCategory(anyInt(), anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.mapCategorytoFeedbackNature("[{\"categoryID\":81,\"feedbackNatureID\":21}]")); + } + + @Test + @DisplayName("updateCategorytoFeedbackNature should clear the old category before mapping the new one") + void updateCategorytoFeedbackNature_shouldClearOldCategoryFirst() { + M_Category previous = category(80, "Previous"); + previous.setFeedbackNatureID(21); + M_Category current = category(81, "Medical"); + when(categoryInter.getcatdatabycatId(80)).thenReturn(previous); + when(categoryInter.getcatdatabycatId(81)).thenReturn(current); + when(categoryInter.deletedata(any())).thenReturn(current); + + String response = controller.updateCategorytoFeedbackNature("{\"oldCategoryID\":80,\"categoryID\":81," + + "\"feedbackNatureID\":22,\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "Medical"); + assertNull(previous.getFeedbackNatureID(), "the old category must stop pointing at a feedback nature"); + assertEquals(22, current.getFeedbackNatureID()); + } + + @Test + @DisplayName("updateCategorytoFeedbackNature should answer an error envelope for an unknown old category") + void updateCategorytoFeedbackNature_shouldAnswerErrorEnvelopeForUnknownOldCategory() { + when(categoryInter.getcatdatabycatId(anyInt())).thenReturn(null); + + assertCodeException( + controller.updateCategorytoFeedbackNature("{\"oldCategoryID\":80,\"categoryID\":81}")); + } + + @Test + @DisplayName("getmapedCategorytoFeedbackNature should answer the categories mapped to the feedback nature") + void getmapedCategorytoFeedbackNature_shouldAnswerMappedCategories() { + ArrayList stored = new ArrayList<>(List.of(category(81, "Medical"))); + when(categoryInter.getAllCategorywithFeedbackNatureID(4001, 21)).thenReturn(stored); + + assertSuccessContaining(controller.getmapedCategorytoFeedbackNatureWithCatIDandFeedbackNatureID( + "{\"providerServiceMapID\":4001,\"feedbackNatureID\":21}"), "Medical"); + } + + @Test + @DisplayName("getmapedCategorytoFeedbackNature should answer an error envelope when the lookup fails") + void getmapedCategorytoFeedbackNature_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.getAllCategorywithFeedbackNatureID(anyInt(), anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getmapedCategorytoFeedbackNatureWithCatIDandFeedbackNatureID( + "{\"providerServiceMapID\":4001,\"feedbackNatureID\":21}")); + } + + @Test + @DisplayName("getunmappedCategoryforFeedbackNature should answer the categories still unmapped") + void getunmappedCategoryforFeedbackNature_shouldAnswerUnmappedCategories() { + ArrayList stored = new ArrayList<>(List.of(category(81, "Medical"))); + when(categoryInter.getUpmappedCategory(4001)).thenReturn(stored); + + assertSuccessContaining( + controller.getunmappedCategoryforFeedbackNature("{\"providerServiceMapID\":4001}"), "Medical"); + } + + @Test + @DisplayName("getunmappedCategoryforFeedbackNature should answer an error envelope when the lookup fails") + void getunmappedCategoryforFeedbackNature_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.getUpmappedCategory(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getunmappedCategoryforFeedbackNature("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("unmappCategoryforFeedbackNature should clear the feedback nature from the category") + void unmappCategoryforFeedbackNature_shouldClearFeedbackNature() { + M_Category stored = category(81, "Medical"); + stored.setFeedbackNatureID(21); + when(categoryInter.getcatdatabycatId(81)).thenReturn(stored); + when(categoryInter.deletedata(stored)).thenReturn(stored); + + assertSuccessContaining( + controller.unmappCategoryforFeedbackNature("{\"categoryID\":81,\"modifiedBy\":\"admin\"}"), "Medical"); + assertNull(stored.getFeedbackNatureID()); + } + + @Test + @DisplayName("unmappCategoryforFeedbackNature should answer an error envelope for an unknown category") + void unmappCategoryforFeedbackNature_shouldAnswerErrorEnvelopeForUnknownCategory() { + when(categoryInter.getcatdatabycatId(81)).thenReturn(null); + + assertCodeException(controller.unmappCategoryforFeedbackNature("{\"categoryID\":81}")); + } + + @Test + @DisplayName("getAllCategoryPsmMapid should answer every category under the mapping") + void getAllCategoryPsmMapid_shouldAnswerEveryCategory() { + ArrayList stored = new ArrayList<>(List.of(category(81, "Medical"))); + when(categoryInter.getAllCategory1(4001)).thenReturn(stored); + + assertSuccessContaining(controller.getAllCategoryPsmMapid("{\"providerServiceMapID\":4001}"), "Medical"); + } + + @Test + @DisplayName("getAllCategoryPsmMapid should answer an error envelope when the lookup fails") + void getAllCategoryPsmMapid_shouldAnswerErrorEnvelopeOnFailure() { + when(categoryInter.getAllCategory1(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllCategoryPsmMapid("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("getServerity should answer the severities configured for the mapping") + void getServerity_shouldAnswerConfiguredSeverities() { + M_Severity severity = new M_Severity(); + severity.setSeverityID(31); + severity.setSeverityTypeName("Critical"); + ArrayList stored = new ArrayList<>(List.of(severity)); + when(m_ServerityInter.getServerity(4001)).thenReturn(stored); + + assertSuccessContaining(controller.getServerity("{\"providerServiceMapID\":4001}"), "Critical"); + } + + @Test + @DisplayName("getServerity should answer an error envelope when the lookup fails") + void getServerity_shouldAnswerErrorEnvelopeOnFailure() { + when(m_ServerityInter.getServerity(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getServerity("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("saveServerity should answer the severities the service stored") + void saveServerity_shouldAnswerStoredSeverities() { + M_Severity severity = new M_Severity(); + severity.setSeverityID(31); + severity.setSeverityTypeName("Critical"); + ArrayList stored = new ArrayList<>(List.of(severity)); + when(m_ServerityInter.saveServerity(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.saveServerity("[{\"severityTypeName\":\"Critical\"}]"), "Critical"); + } + + @Test + @DisplayName("saveServerity should answer an error envelope when the store fails") + void saveServerity_shouldAnswerErrorEnvelopeOnFailure() { + when(m_ServerityInter.saveServerity(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.saveServerity("[{\"severityTypeName\":\"Critical\"}]")); + } + + @Test + @DisplayName("deleteServerity should mark the severity deleted and answer what was saved") + void deleteServerity_shouldMarkSeverityDeleted() { + M_Severity stored = new M_Severity(); + stored.setSeverityID(31); + stored.setSeverityTypeName("Critical"); + when(m_ServerityInter.getDataByServId(31)).thenReturn(stored); + when(m_ServerityInter.deletedataser(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteServerity("{\"severityID\":31,\"deleted\":true}"), "Critical"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteServerity should answer an error envelope for a severity that does not exist") + void deleteServerity_shouldAnswerErrorEnvelopeForUnknownSeverity() { + when(m_ServerityInter.getDataByServId(31)).thenReturn(null); + + assertCodeException(controller.deleteServerity("{\"severityID\":31,\"deleted\":true}")); + } + + @Test + @DisplayName("editServerity should copy the edited fields onto the stored severity") + void editServerity_shouldCopyEditedFields() { + M_Severity stored = new M_Severity(); + stored.setSeverityID(31); + when(m_ServerityInter.getDataByServId(31)).thenReturn(stored); + when(m_ServerityInter.deletedataser(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editServerity("{\"severityID\":31,\"severityTypeName\":\"Critical\"," + + "\"severityDesc\":\"Needs escalation\"}"), "Critical"); + assertEquals("Needs escalation", stored.getSeverityDesc()); + } + + @Test + @DisplayName("editServerity should answer an error envelope for a severity that does not exist") + void editServerity_shouldAnswerErrorEnvelopeForUnknownSeverity() { + when(m_ServerityInter.getDataByServId(31)).thenReturn(null); + + assertCodeException(controller.editServerity("{\"severityID\":31}")); + } + + @Test + @DisplayName("getFeedbackType should answer the feedback types configured for the mapping") + void getFeedbackType_shouldAnswerConfiguredFeedbackTypes() { + M_Feedbacktype feedbackType = new M_Feedbacktype(); + feedbackType.setFeedbackTypeID(41); + feedbackType.setFeedbackTypeName("Complaint"); + ArrayList stored = new ArrayList<>(List.of(feedbackType)); + when(m_FeedbacktypeInter.getFeedbackt(4001)).thenReturn(stored); + + assertSuccessContaining(controller.getFeedbackType("{\"providerServiceMapID\":4001}"), "Complaint"); + } + + @Test + @DisplayName("getFeedbackType should answer an error envelope when the lookup fails") + void getFeedbackType_shouldAnswerErrorEnvelopeOnFailure() { + when(m_FeedbacktypeInter.getFeedbackt(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getFeedbackType("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("saveFeedbackType should answer the feedback types the service stored") + void saveFeedbackType_shouldAnswerStoredFeedbackTypes() { + M_Feedbacktype feedbackType = new M_Feedbacktype(); + feedbackType.setFeedbackTypeID(41); + feedbackType.setFeedbackTypeName("Complaint"); + ArrayList stored = new ArrayList<>(List.of(feedbackType)); + when(m_FeedbacktypeInter.saveFeedbackType(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.saveFeedbackType("[{\"feedbackTypeName\":\"Complaint\"}]"), "Complaint"); + } + + @Test + @DisplayName("saveFeedbackType should answer an error envelope when the store fails") + void saveFeedbackType_shouldAnswerErrorEnvelopeOnFailure() { + when(m_FeedbacktypeInter.saveFeedbackType(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.saveFeedbackType("[{\"feedbackTypeName\":\"Complaint\"}]")); + } + + @Test + @DisplayName("editFeedbackType should copy the edited fields onto the stored feedback type") + void editFeedbackType_shouldCopyEditedFields() { + M_Feedbacktype stored = new M_Feedbacktype(); + stored.setFeedbackTypeID(41); + when(m_FeedbacktypeInter.getDataByServId(41)).thenReturn(stored); + when(m_FeedbacktypeInter.deletedataser(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editFeedbackType("{\"feedbackTypeID\":41," + + "\"feedbackTypeName\":\"Complaint\",\"feedbackDesc\":\"Service issue\"," + + "\"modifiedBy\":\"admin\"}"), "Complaint"); + assertEquals("Service issue", stored.getFeedbackDesc()); + } + + @Test + @DisplayName("editFeedbackType should answer an error envelope for a feedback type that does not exist") + void editFeedbackType_shouldAnswerErrorEnvelopeForUnknownFeedbackType() { + when(m_FeedbacktypeInter.getDataByServId(41)).thenReturn(null); + + assertCodeException(controller.editFeedbackType("{\"feedbackTypeID\":41}")); + } + + @Test + @DisplayName("deleteFeedbackType should mark the feedback type deleted") + void deleteFeedbackType_shouldMarkFeedbackTypeDeleted() { + M_Feedbacktype stored = new M_Feedbacktype(); + stored.setFeedbackTypeID(41); + stored.setFeedbackTypeName("Complaint"); + when(m_FeedbacktypeInter.getDataByServId(41)).thenReturn(stored); + when(m_FeedbacktypeInter.deletedataser(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteFeedbackType("{\"feedbackTypeID\":41,\"deleted\":true}"), + "Complaint"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteFeedbackType should answer an error envelope for a feedback type that does not exist") + void deleteFeedbackType_shouldAnswerErrorEnvelopeForUnknownFeedbackType() { + when(m_FeedbacktypeInter.getDataByServId(41)).thenReturn(null); + + assertCodeException(controller.deleteFeedbackType("{\"feedbackTypeID\":41,\"deleted\":true}")); + } + + @Test + @DisplayName("getFeedbackNatureType should answer the natures under the feedback type") + void getFeedbackNatureType_shouldAnswerNatures() { + M_Feedbacknature nature = new M_Feedbacknature(); + nature.setFeedbackNatureID(21); + nature.setFeedbackNature("Escalation"); + ArrayList stored = new ArrayList<>(List.of(nature)); + when(m_FeedbacknatureInteger.getFeedbackNatureType(41)).thenReturn(stored); + + assertSuccessContaining(controller.getFeedbackNatureType("{\"feedbackTypeID\":41}"), "Escalation"); + } + + @Test + @DisplayName("getFeedbackNatureType should answer an error envelope when the lookup fails") + void getFeedbackNatureType_shouldAnswerErrorEnvelopeOnFailure() { + when(m_FeedbacknatureInteger.getFeedbackNatureType(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getFeedbackNatureType("{\"feedbackTypeID\":41}")); + } + + @Test + @DisplayName("createFeedbackNatureType should answer the natures the service stored") + void createFeedbackNatureType_shouldAnswerStoredNatures() { + M_Feedbacknature nature = new M_Feedbacknature(); + nature.setFeedbackNatureID(21); + nature.setFeedbackNature("Escalation"); + ArrayList stored = new ArrayList<>(List.of(nature)); + when(m_FeedbacknatureInteger.createFeedbackNatueType(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.createFeedbackNatureType("[{\"feedbackNature\":\"Escalation\"}]"), + "Escalation"); + } + + @Test + @DisplayName("createFeedbackNatureType should answer an error envelope when the store fails") + void createFeedbackNatureType_shouldAnswerErrorEnvelopeOnFailure() { + when(m_FeedbacknatureInteger.createFeedbackNatueType(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createFeedbackNatureType("[{\"feedbackNature\":\"Escalation\"}]")); + } + + @Test + @DisplayName("deleteFeedbackNatureType should mark the nature deleted") + void deleteFeedbackNatureType_shouldMarkNatureDeleted() { + M_Feedbacknature stored = new M_Feedbacknature(); + stored.setFeedbackNatureID(21); + stored.setFeedbackNature("Escalation"); + when(m_FeedbacknatureInteger.editFeedbackNatureType(21)).thenReturn(stored); + when(m_FeedbacknatureInteger.saveEditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteFeedbackNatureType("{\"feedbackNatureID\":21,\"deleted\":true}"), + "Escalation"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteFeedbackNatureType should answer an error envelope for a nature that does not exist") + void deleteFeedbackNatureType_shouldAnswerErrorEnvelopeForUnknownNature() { + when(m_FeedbacknatureInteger.editFeedbackNatureType(21)).thenReturn(null); + + assertCodeException(controller.deleteFeedbackNatureType("{\"feedbackNatureID\":21,\"deleted\":true}")); + } + + @Test + @DisplayName("editFeedbackNatureType should copy the edited fields onto the stored nature") + void editFeedbackNatureType_shouldCopyEditedFields() { + M_Feedbacknature stored = new M_Feedbacknature(); + stored.setFeedbackNatureID(21); + when(m_FeedbacknatureInteger.editFeedbackNatureType(21)).thenReturn(stored); + when(m_FeedbacknatureInteger.saveEditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editFeedbackNatureType("{\"feedbackNatureID\":21," + + "\"feedbackNature\":\"Escalation\",\"feedbackNatureDesc\":\"Raise to supervisor\"," + + "\"modifiedBy\":\"admin\"}"), "Escalation"); + assertEquals("Raise to supervisor", stored.getFeedbackNatureDesc()); + } + + @Test + @DisplayName("editFeedbackNatureType should answer an error envelope for a nature that does not exist") + void editFeedbackNatureType_shouldAnswerErrorEnvelopeForUnknownNature() { + when(m_FeedbacknatureInteger.editFeedbackNatureType(21)).thenReturn(null); + + assertCodeException(controller.editFeedbackNatureType("{\"feedbackNatureID\":21}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardDrugControllerTest.java b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardDrugControllerTest.java new file mode 100644 index 0000000..d52347d --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardDrugControllerTest.java @@ -0,0 +1,304 @@ +/* +* 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.admin.controller.provideronboard; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.admin.data.provideronboard.M_104druggroup; +import com.iemr.admin.data.provideronboard.M_104drugmapping; +import com.iemr.admin.data.provideronboard.M_104drugmaster; +import com.iemr.admin.utils.exception.IEMRException; + +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.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** The drug endpoints keep the 104 helpline's drug catalogue for a provider. */ +@DisplayName("ProviderOnBoardController drug Test Suite") +class ProviderOnBoardDrugControllerTest extends ProviderOnBoardFixture { + + private static M_104drugmaster drug(Integer id, String name) { + M_104drugmaster drug = new M_104drugmaster(); + drug.setDrugID(id); + drug.setDrugName(name); + return drug; + } + + private static M_104druggroup drugGroup(Integer id, String name) { + M_104druggroup group = new M_104druggroup(); + group.setDrugGroupID(id); + group.setDrugGroup(name); + return group; + } + + private static M_104drugmapping drugMapping(Integer id, String drugName) { + M_104drugmapping mapping = new M_104drugmapping(); + mapping.setDrugMapID(id); + mapping.setDrugName(drugName); + return mapping; + } + + @Test + @DisplayName("getDrugData should answer the drugs the catalogue holds") + void getDrugData_shouldAnswerCatalogueDrugs() throws IEMRException { + ArrayList stored = new ArrayList<>(List.of(drug(101, "Paracetamol"))); + when(drugMasterInter.getAllDrugData(101, (short) 77, Boolean.FALSE)).thenReturn(stored); + + assertSuccessContaining( + controller.getDrugData("{\"drugID\":101,\"serviceProviderID\":77,\"deleted\":false}"), + "Paracetamol"); + } + + @Test + @DisplayName("getDrugData should answer an error envelope when the catalogue cannot be read") + void getDrugData_shouldAnswerErrorEnvelopeOnFailure() throws IEMRException { + when(drugMasterInter.getAllDrugData(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getDrugData("{\"drugID\":101,\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getDrugGroups should answer the drug groups the catalogue holds") + void getDrugGroups_shouldAnswerCatalogueGroups() throws IEMRException { + ArrayList stored = new ArrayList<>(List.of(drugGroup(201, "Analgesics"))); + when(drugMasterInter.getAllDrugGroups(201, (short) 77, Boolean.FALSE)).thenReturn(stored); + + assertSuccessContaining( + controller.getDrugGroups("{\"drugGroupID\":201,\"serviceProviderID\":77,\"deleted\":false}"), + "Analgesics"); + } + + @Test + @DisplayName("getDrugGroups should answer an error envelope when the catalogue cannot be read") + void getDrugGroups_shouldAnswerErrorEnvelopeOnFailure() throws IEMRException { + when(drugMasterInter.getAllDrugGroups(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getDrugGroups("{\"drugGroupID\":201,\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getDrugGroupMappings should answer every mapping for the provider's service") + void getDrugGroupMappings_shouldAnswerMappings() throws IEMRException { + ArrayList stored = new ArrayList<>(List.of(drugMapping(301, "Paracetamol"))); + when(drugMasterInter.getAllDrugGroupMappings(null, 77, 3)).thenReturn(stored); + + assertSuccessContaining( + controller.getDrugGroupMappings("{\"serviceProviderID\":77,\"serviceID\":3}"), "Paracetamol"); + } + + @Test + @DisplayName("getDrugGroupMappings should answer an error envelope when the lookup fails") + void getDrugGroupMappings_shouldAnswerErrorEnvelopeOnFailure() throws IEMRException { + when(drugMasterInter.getAllDrugGroupMappings(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getDrugGroupMappings("{\"serviceProviderID\":77,\"serviceID\":3}")); + } + + @Test + @DisplayName("updateDrugGroup should copy the edited fields onto the stored group") + void updateDrugGroup_shouldCopyEditedFields() { + M_104druggroup stored = drugGroup(201, "old name"); + when(drugMasterInter.getDrugGroupById(201)).thenReturn(stored); + when(drugMasterInter.saveUpdatedDrugGroup(stored)).thenReturn(stored); + + assertSuccessContaining(controller.updateDrugGroup("{\"drugGroupID\":201,\"drugGroup\":\"Analgesics\"," + + "\"drugGroupDesc\":\"Pain relief\",\"modifiedBy\":\"admin\"}"), "Analgesics"); + assertEquals("Pain relief", stored.getDrugGroupDesc()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("updateDrugGroup should answer an error envelope for a group that does not exist") + void updateDrugGroup_shouldAnswerErrorEnvelopeForUnknownGroup() { + when(drugMasterInter.getDrugGroupById(201)).thenReturn(null); + + assertCodeException(controller.updateDrugGroup("{\"drugGroupID\":201}")); + } + + @Test + @DisplayName("updateDrugMaster should copy the edited fields onto the stored drug") + void updateDrugMaster_shouldCopyEditedFields() { + M_104drugmaster stored = drug(101, "old name"); + when(drugMasterInter.getDrugDataById(101)).thenReturn(stored); + when(drugMasterInter.saveUpdatedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.updateDrugMaster("{\"drugID\":101,\"drugName\":\"Paracetamol\"," + + "\"drugDesc\":\"Antipyretic\",\"remarks\":\"OTC\",\"modifiedBy\":\"admin\"}"), "Paracetamol"); + assertEquals("Antipyretic", stored.getDrugDesc()); + assertEquals("OTC", stored.getRemarks()); + } + + @Test + @DisplayName("updateDrugMaster should answer an error envelope for a drug that does not exist") + void updateDrugMaster_shouldAnswerErrorEnvelopeForUnknownDrug() { + when(drugMasterInter.getDrugDataById(101)).thenReturn(null); + + assertCodeException(controller.updateDrugMaster("{\"drugID\":101}")); + } + + @Test + @DisplayName("updateDrugMapping should copy the edited fields onto the stored mapping") + void updateDrugMapping_shouldCopyEditedFields() { + M_104drugmapping stored = drugMapping(301, "old name"); + when(drugMasterInter.getDrugMappingsById(301)).thenReturn(stored); + when(drugMasterInter.saveUpdatedDrugMapping(stored)).thenReturn(stored); + + assertSuccessContaining(controller.updateDrugMapping("{\"drugMapID\":301,\"drugGroupID\":201," + + "\"drugGroupName\":\"Analgesics\",\"drugId\":101,\"drugName\":\"Paracetamol\"," + + "\"remarks\":\"OTC\"}"), "Paracetamol"); + assertEquals(201, stored.getDrugGroupID()); + assertEquals("Analgesics", stored.getDrugGroupName()); + } + + @Test + @DisplayName("updateDrugMapping should answer an error envelope for a mapping that does not exist") + void updateDrugMapping_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(drugMasterInter.getDrugMappingsById(301)).thenReturn(null); + + assertCodeException(controller.updateDrugMapping("{\"drugMapID\":301}")); + } + + @Test + @DisplayName("updateDrugStatus should retire the drug when the request names one") + void updateDrugStatus_shouldRetireDrug() { + when(drugMasterInter.updateDrugStatus(any())).thenReturn(1); + + assertSuccessContaining(controller.updateDrugStatus("{\"drugID\":101,\"deleted\":true}"), + "Drug status updaated to deleted"); + verify(drugMasterInter).updateDrugStatus(any()); + verify(drugMasterInter, never()).updateDrugGroupStatus(any()); + } + + @Test + @DisplayName("updateDrugStatus should retire the drug group when no drug is named") + void updateDrugStatus_shouldRetireDrugGroup() { + when(drugMasterInter.updateDrugGroupStatus(any())).thenReturn(1); + + assertSuccessContaining(controller.updateDrugStatus("{\"drugGroupID\":201,\"deleted\":true}"), + "DrugGroup status updaated to deleted"); + verify(drugMasterInter).updateDrugGroupStatus(any()); + verify(drugMasterInter, never()).updateDrugStatus(any()); + } + + @Test + @DisplayName("updateDrugStatus should retire the mapping when neither a drug nor a group is named") + void updateDrugStatus_shouldRetireDrugMapping() { + when(drugMasterInter.updateDrugMappingStatus(any())).thenReturn(1); + + assertSuccessContaining(controller.updateDrugStatus("{\"drugMapID\":301,\"deleted\":true}"), + "DrugGroup status updaated to deleted"); + verify(drugMasterInter).updateDrugMappingStatus(any()); + } + + @Test + @DisplayName("updateDrugStatus should answer an empty response when the request names nothing to retire") + void updateDrugStatus_shouldAnswerEmptyResponseWhenNothingNamed() { + assertSuccessContaining(controller.updateDrugStatus("{}"), "response"); + verify(drugMasterInter, never()).updateDrugStatus(any()); + verify(drugMasterInter, never()).updateDrugGroupStatus(any()); + verify(drugMasterInter, never()).updateDrugMappingStatus(any()); + } + + @Test + @DisplayName("updateDrugStatus should answer an error envelope when the retirement fails") + void updateDrugStatus_shouldAnswerErrorEnvelopeOnFailure() { + when(drugMasterInter.updateDrugStatus(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.updateDrugStatus("{\"drugID\":101,\"deleted\":true}")); + } + + @Test + @DisplayName("saveDrugGroup should answer the groups the service stored") + void saveDrugGroup_shouldAnswerStoredGroups() throws IEMRException { + ArrayList stored = new ArrayList<>(List.of(drugGroup(201, "Analgesics"))); + when(drugMasterInter.saveDrugGroup(anyList())).thenReturn(stored); + + assertSuccessContaining( + controller.saveDrugGroup("{\"drugGroups\":[{\"drugGroup\":\"Analgesics\"}]}"), "Analgesics"); + } + + @Test + @DisplayName("saveDrugGroup should answer an error envelope when no groups are named") + void saveDrugGroup_shouldAnswerErrorEnvelopeWithoutGroups() throws IEMRException { + when(drugMasterInter.saveDrugGroup(eq(null))).thenThrow(new IllegalArgumentException("nothing to save")); + + assertGenericFailure(controller.saveDrugGroup("{}")); + } + + @Test + @DisplayName("saveDrug should answer the drugs the service stored") + void saveDrug_shouldAnswerStoredDrugs() throws IEMRException { + ArrayList stored = new ArrayList<>(List.of(drug(101, "Paracetamol"))); + when(drugMasterInter.saveDrugData(anyList())).thenReturn(stored); + + assertSuccessContaining( + controller.saveDrug("{\"drugMasters\":[{\"drugName\":\"Paracetamol\"}]}"), "Paracetamol"); + } + + @Test + @DisplayName("saveDrug should answer an error envelope when the store fails") + void saveDrug_shouldAnswerErrorEnvelopeOnFailure() throws IEMRException { + when(drugMasterInter.saveDrugData(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.saveDrug("{\"drugMasters\":[{\"drugName\":\"Paracetamol\"}]}")); + } + + @Test + @DisplayName("mapDrugWithGroup should answer the mappings the service stored") + void mapDrugWithGroup_shouldAnswerStoredMappings() throws IEMRException { + ArrayList stored = new ArrayList<>(List.of(drugMapping(301, "Paracetamol"))); + when(drugMasterInter.mapDrugWithGroup(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.mapDrugWithGroup( + "{\"drugMappings\":[{\"drugId\":101,\"drugGroupID\":201}]}"), "Paracetamol"); + } + + @Test + @DisplayName("mapDrugWithGroup should answer an error envelope when the mapping fails") + void mapDrugWithGroup_shouldAnswerErrorEnvelopeOnFailure() throws IEMRException { + when(drugMasterInter.mapDrugWithGroup(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.mapDrugWithGroup("{\"drugMappings\":[{\"drugId\":101}]}")); + } + + @Test + @DisplayName("getDrugData should pass the deleted flag the caller sends straight through") + void getDrugData_shouldPassDeletedFlagThrough() throws IEMRException { + when(drugMasterInter.getAllDrugData(anyInt(), any(), eq(Boolean.TRUE))).thenReturn(new ArrayList<>()); + + controller.getDrugData("{\"drugID\":101,\"serviceProviderID\":77,\"deleted\":true}"); + + verify(drugMasterInter).getAllDrugData(101, (short) 77, Boolean.TRUE); + } +} diff --git a/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardFixture.java b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardFixture.java new file mode 100644 index 0000000..6047603 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardFixture.java @@ -0,0 +1,131 @@ +/* +* 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.admin.controller.provideronboard; + +import org.json.JSONObject; +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.admin.service.provideronboard.Calltypeinter; +import com.iemr.admin.service.provideronboard.CategoryInter; +import com.iemr.admin.service.provideronboard.DrugMasterInter; +import com.iemr.admin.service.provideronboard.InstuteDirectoryInter; +import com.iemr.admin.service.provideronboard.M_FeedbacknatureInteger; +import com.iemr.admin.service.provideronboard.M_FeedbacktypeInter; +import com.iemr.admin.service.provideronboard.M_InstitutedirectorymappingInter; +import com.iemr.admin.service.provideronboard.M_InstitutesubdirectoryInter; +import com.iemr.admin.service.provideronboard.M_InstitutionInter; +import com.iemr.admin.service.provideronboard.M_InstitutiontypeInter; +import com.iemr.admin.service.provideronboard.M_ServiceMasterInter; +import com.iemr.admin.service.provideronboard.M_SeverityInter; +import com.iemr.admin.service.provideronboard.ServiceProvider_ServiceImpl; +import com.iemr.admin.service.provideronboard.SubServiceInter; +import com.iemr.admin.service.user.IemrUserServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The onboarding controller reaches thirteen collaborating services, so every + * suite over it shares this fixture rather than re-declaring the same mocks. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +abstract class ProviderOnBoardFixture { + + @Mock + protected M_InstitutedirectorymappingInter m_InstitutedirectorymappingInter; + + @Mock + protected M_InstitutesubdirectoryInter m_InstitutesubdirectoryInter; + + @Mock + protected M_InstitutionInter m_InstitutionInter; + + @Mock + protected M_FeedbacknatureInteger m_FeedbacknatureInteger; + + @Mock + protected M_InstitutiontypeInter m_InstitutiontypeInter; + + @Mock + protected InstuteDirectoryInter instuteDirectoryInter; + + @Mock + protected M_FeedbacktypeInter m_FeedbacktypeInter; + + @Mock + protected M_SeverityInter m_ServerityInter; + + @Mock + protected DrugMasterInter drugMasterInter; + + @Mock + protected CategoryInter categoryInter; + + @Mock + protected SubServiceInter subServiceInter; + + @Mock + protected Calltypeinter calltypeinter; + + @Mock + protected M_ServiceMasterInter m_ServiceMasterInter; + + @Mock + protected ServiceProvider_ServiceImpl serviceProvider_ServiceImpl; + + @Mock + protected IemrUserServiceImpl iemrUserServiceImpl; + + @InjectMocks + protected ProviderOnBoardController controller; + + /** Reads the status code out of the JSON envelope the controller answers with. */ + protected static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + /** Asserts the envelope reports success and carries the given fragment. */ + protected static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + /** Asserts the envelope reports the generic failure the controllers fall back to. */ + protected static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + /** + * Asserts the envelope reports the code-level failure raised when a controller + * works on a record the service could not resolve. + */ + protected static void assertCodeException(String response) { + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(response), response); + } +} diff --git a/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardInstituteControllerTest.java b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardInstituteControllerTest.java new file mode 100644 index 0000000..286ca6c --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardInstituteControllerTest.java @@ -0,0 +1,557 @@ +/* +* 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.admin.controller.provideronboard; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.google.gson.JsonObject; +import com.iemr.admin.data.provideronboard.M_Institutedirectory; +import com.iemr.admin.data.provideronboard.M_Institutedirectorymapping; +import com.iemr.admin.data.provideronboard.M_Institutesubdirectory; +import com.iemr.admin.data.provideronboard.M_Institution; +import com.iemr.admin.data.provideronboard.M_Institutiontype; + +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.Mockito.when; + +/** + * The institute endpoints keep the referral directory a provider's agents route + * callers to - the directories, their sub-directories, and the institutions + * mapped underneath them. + */ +@DisplayName("ProviderOnBoardController institute Test Suite") +class ProviderOnBoardInstituteControllerTest extends ProviderOnBoardFixture { + + private static M_Institutedirectory directory(Integer id, String name) { + M_Institutedirectory directory = new M_Institutedirectory(); + directory.setInstituteDirectoryID(id); + directory.setInstituteDirectoryName(name); + return directory; + } + + private static M_Institutiontype instituteType(Integer id, String name) { + M_Institutiontype type = new M_Institutiontype(); + type.setInstitutionTypeID(id); + type.setInstitutionType(name); + return type; + } + + private static M_Institution institution(Integer id, String name) { + M_Institution institution = new M_Institution(); + institution.setInstitutionID(id); + institution.setInstitutionName(name); + return institution; + } + + private static M_Institutesubdirectory subDirectory(Integer id, String name) { + M_Institutesubdirectory subDirectory = new M_Institutesubdirectory(); + subDirectory.setInstituteSubDirectoryID(id); + subDirectory.setInstituteSubDirectoryName(name); + return subDirectory; + } + + private static M_Institutedirectorymapping directoryMapping(Integer id, Integer institutionId) { + M_Institutedirectorymapping mapping = new M_Institutedirectorymapping(); + mapping.setInstituteDirMapID(id); + mapping.setInstitutionID(institutionId); + return mapping; + } + + @Test + @DisplayName("createInstuteDirectoty should answer the directories the service stored") + void createInstuteDirectoty_shouldAnswerStoredDirectories() { + ArrayList stored = new ArrayList<>(List.of(directory(11, "Hospitals"))); + when(instuteDirectoryInter.createInstuteDirectory(anyList())).thenReturn(stored); + + assertSuccessContaining( + controller.createInstuteDirectoty("[{\"instituteDirectoryName\":\"Hospitals\"}]"), "Hospitals"); + } + + @Test + @DisplayName("createInstuteDirectoty should answer an error envelope when the store fails") + void createInstuteDirectoty_shouldAnswerErrorEnvelopeOnFailure() { + when(instuteDirectoryInter.createInstuteDirectory(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createInstuteDirectoty("[{\"instituteDirectoryName\":\"Hospitals\"}]")); + } + + @Test + @DisplayName("getInstuteDirectory should answer the directories configured for the mapping") + void getInstuteDirectory_shouldAnswerConfiguredDirectories() { + ArrayList stored = new ArrayList<>(List.of(directory(11, "Hospitals"))); + when(instuteDirectoryInter.getInstuteDirectory(4001)).thenReturn(stored); + + assertSuccessContaining(controller.getInstuteDirectory("{\"providerServiceMapId\":4001}"), "Hospitals"); + } + + @Test + @DisplayName("getInstuteDirectory should answer an error envelope when the lookup fails") + void getInstuteDirectory_shouldAnswerErrorEnvelopeOnFailure() { + when(instuteDirectoryInter.getInstuteDirectory(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getInstuteDirectory("{\"providerServiceMapId\":4001}")); + } + + @Test + @DisplayName("editInstuteDirectory should copy the edited fields onto the stored directory") + void editInstuteDirectory_shouldCopyEditedFields() { + M_Institutedirectory stored = directory(11, "old name"); + when(instuteDirectoryInter.editInstuteDirectory(11)).thenReturn(stored); + when(instuteDirectoryInter.editdata(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editInstuteDirectory("{\"instituteDirectoryID\":11," + + "\"instituteDirectoryName\":\"Hospitals\",\"instituteDirectoryDesc\":\"Referral hospitals\"," + + "\"modifiedBy\":\"admin\"}"), "Hospitals"); + assertEquals("Referral hospitals", stored.getInstituteDirectoryDesc()); + } + + @Test + @DisplayName("editInstuteDirectory should answer an error envelope for a directory that does not exist") + void editInstuteDirectory_shouldAnswerErrorEnvelopeForUnknownDirectory() { + when(instuteDirectoryInter.editInstuteDirectory(11)).thenReturn(null); + + assertCodeException(controller.editInstuteDirectory("{\"instituteDirectoryID\":11}")); + } + + @Test + @DisplayName("deleteInstuteDirectory should mark the directory deleted") + void deleteInstuteDirectory_shouldMarkDirectoryDeleted() { + M_Institutedirectory stored = directory(11, "Hospitals"); + when(instuteDirectoryInter.editInstuteDirectory(11)).thenReturn(stored); + when(instuteDirectoryInter.editdata(stored)).thenReturn(stored); + + assertSuccessContaining( + controller.deleteInstuteDirectory("{\"instituteDirectoryID\":11,\"deleted\":true}"), "Hospitals"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteInstuteDirectory should answer an error envelope for a directory that does not exist") + void deleteInstuteDirectory_shouldAnswerErrorEnvelopeForUnknownDirectory() { + when(instuteDirectoryInter.editInstuteDirectory(11)).thenReturn(null); + + assertCodeException(controller.deleteInstuteDirectory("{\"instituteDirectoryID\":11,\"deleted\":true}")); + } + + @Test + @DisplayName("getInstuteType should answer the institute types configured for the mapping") + void getInstuteType_shouldAnswerConfiguredTypes() { + ArrayList stored = new ArrayList<>(List.of(instituteType(21, "PHC"))); + when(m_InstitutiontypeInter.getInstuteType(4001)).thenReturn(stored); + + assertSuccessContaining(controller.getInstuteType("{\"providerServiceMapID\":4001}"), "PHC"); + } + + @Test + @DisplayName("getInstuteType should answer an error envelope when the lookup fails") + void getInstuteType_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutiontypeInter.getInstuteType(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getInstuteType("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("createInstuteType should answer the institute types the service stored") + void createInstuteType_shouldAnswerStoredTypes() { + ArrayList stored = new ArrayList<>(List.of(instituteType(21, "PHC"))); + when(m_InstitutiontypeInter.createInstuteType(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.createInstuteType("[{\"institutionType\":\"PHC\"}]"), "PHC"); + } + + @Test + @DisplayName("createInstuteType should answer an error envelope when the store fails") + void createInstuteType_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutiontypeInter.createInstuteType(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createInstuteType("[{\"institutionType\":\"PHC\"}]")); + } + + @Test + @DisplayName("editInstuteType should copy the edited fields onto the stored institute type") + void editInstuteType_shouldCopyEditedFields() { + M_Institutiontype stored = instituteType(21, "old name"); + when(m_InstitutiontypeInter.editInstuteType(21)).thenReturn(stored); + when(m_InstitutiontypeInter.saveEditdata(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editInstuteType("{\"institutionTypeID\":21,\"institutionType\":\"PHC\"," + + "\"institutionTypeDesc\":\"Primary health centre\",\"modifiedBy\":\"admin\"}"), "PHC"); + assertEquals("Primary health centre", stored.getInstitutionTypeDesc()); + } + + @Test + @DisplayName("editInstuteType should answer an error envelope for an institute type that does not exist") + void editInstuteType_shouldAnswerErrorEnvelopeForUnknownType() { + when(m_InstitutiontypeInter.editInstuteType(21)).thenReturn(null); + + assertCodeException(controller.editInstuteType("{\"institutionTypeID\":21}")); + } + + @Test + @DisplayName("deleteInstuteType should mark the institute type deleted") + void deleteInstuteType_shouldMarkTypeDeleted() { + M_Institutiontype stored = instituteType(21, "PHC"); + when(m_InstitutiontypeInter.editInstuteType(21)).thenReturn(stored); + when(m_InstitutiontypeInter.saveEditdata(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteInstuteType("{\"institutionTypeID\":21,\"deleted\":true}"), "PHC"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteInstuteType should answer an error envelope for an institute type that does not exist") + void deleteInstuteType_shouldAnswerErrorEnvelopeForUnknownType() { + when(m_InstitutiontypeInter.editInstuteType(21)).thenReturn(null); + + assertCodeException(controller.deleteInstuteType("{\"institutionTypeID\":21,\"deleted\":true}")); + } + + @Test + @DisplayName("createInstuteTypeByDist should answer the institute types the service stored") + void createInstuteTypeByDist_shouldAnswerStoredTypes() { + ArrayList stored = new ArrayList<>(List.of(instituteType(21, "PHC"))); + when(m_InstitutiontypeInter.createInstuteType(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.createInstuteTypeByDist("[{\"institutionType\":\"PHC\"}]"), "PHC"); + } + + @Test + @DisplayName("createInstuteTypeByDist should answer an error envelope when the store fails") + void createInstuteTypeByDist_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutiontypeInter.createInstuteType(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createInstuteTypeByDist("[{\"institutionType\":\"PHC\"}]")); + } + + @Test + @DisplayName("getInstituteTypeByDist should answer the institute types under the location") + void getInstituteTypeByDist_shouldAnswerTypesUnderLocation() { + ArrayList stored = new ArrayList<>(List.of(instituteType(21, "PHC"))); + when(m_InstitutiontypeInter.getInstuteTypeByDist(4001, 31, 41, 51)).thenReturn(stored); + + assertSuccessContaining(controller.getInstituteTypeByDist("{\"providerServiceMapID\":4001," + + "\"districtId\":31,\"subDistrictId\":41,\"villageId\":51}"), "PHC"); + } + + @Test + @DisplayName("getInstituteTypeByDist should answer an error envelope when the lookup fails") + void getInstituteTypeByDist_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutiontypeInter.getInstuteTypeByDist(any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getInstituteTypeByDist("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("getInstution should answer the institutions under the block") + void getInstution_shouldAnswerInstitutionsUnderBlock() { + ArrayList stored = new ArrayList<>(List.of(institution(31, "District Hospital"))); + when(m_InstitutionInter.getInstution(4001, 29, 301, 401)).thenReturn(stored); + + assertSuccessContaining(controller.getInstution("{\"providerServiceMapID\":4001,\"stateID\":29," + + "\"districtID\":301,\"blockID\":401}"), "District Hospital"); + } + + @Test + @DisplayName("getInstution should answer an error envelope when the lookup fails") + void getInstution_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutionInter.getInstution(any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getInstution("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("createInstution should answer the institutions the service stored") + void createInstution_shouldAnswerStoredInstitutions() { + ArrayList stored = new ArrayList<>(List.of(institution(31, "District Hospital"))); + when(m_InstitutionInter.createInstution(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.createInstution("[{\"institutionName\":\"District Hospital\"}]"), + "District Hospital"); + } + + @Test + @DisplayName("createInstution should answer an error envelope when the store fails") + void createInstution_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutionInter.createInstution(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createInstution("[{\"institutionName\":\"District Hospital\"}]")); + } + + @Test + @DisplayName("editInstution should copy every contact field onto the stored institution") + void editInstution_shouldCopyContactFields() { + M_Institution stored = institution(31, "old name"); + when(m_InstitutionInter.editInstution(31)).thenReturn(stored); + when(m_InstitutionInter.saveEditData(stored)).thenReturn(stored); + + String response = controller.editInstution("{\"institutionID\":31," + + "\"institutionName\":\"District Hospital\",\"address\":\"Main Road\",\"contactNo1\":\"9000000001\"," + + "\"contactNo2\":\"9000000002\",\"contactNo3\":\"9000000003\",\"contactPerson1\":\"Asha\"," + + "\"contactPerson2\":\"Ravi\",\"contactPerson3\":\"Meera\"," + + "\"contactPerson1_Email\":\"asha@example.org\",\"contactPerson2_Email\":\"ravi@example.org\"," + + "\"contactPerson3_Email\":\"meera@example.org\",\"website\":\"https://example.org\"}"); + + assertSuccessContaining(response, "District Hospital"); + assertEquals("Main Road", stored.getAddress()); + assertEquals("asha@example.org", stored.getContactPerson1_Email()); + assertEquals("https://example.org", stored.getWebsite()); + } + + @Test + @DisplayName("editInstution should answer an error envelope for an institution that does not exist") + void editInstution_shouldAnswerErrorEnvelopeForUnknownInstitution() { + when(m_InstitutionInter.editInstution(31)).thenReturn(null); + + assertCodeException(controller.editInstution("{\"institutionID\":31}")); + } + + @Test + @DisplayName("deleteInstution should mark the institution deleted") + void deleteInstution_shouldMarkInstitutionDeleted() { + M_Institution stored = institution(31, "District Hospital"); + when(m_InstitutionInter.editInstution(31)).thenReturn(stored); + when(m_InstitutionInter.saveEditData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteInstution("{\"institutionID\":31,\"deleted\":true}"), + "District Hospital"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteInstution should answer an error envelope for an institution that does not exist") + void deleteInstution_shouldAnswerErrorEnvelopeForUnknownInstitution() { + when(m_InstitutionInter.editInstution(31)).thenReturn(null); + + assertCodeException(controller.deleteInstution("{\"institutionID\":31,\"deleted\":true}")); + } + + @Test + @DisplayName("createInstutionByVillage should answer the institutions the service stored") + void createInstutionByVillage_shouldAnswerStoredInstitutions() { + ArrayList stored = new ArrayList<>(List.of(institution(31, "Sub Centre"))); + when(m_InstitutionInter.createInstutionByVillage(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.createInstutionByVillage("[{\"institutionName\":\"Sub Centre\"}]"), + "Sub Centre"); + } + + @Test + @DisplayName("createInstutionByVillage should answer an error envelope when the store fails") + void createInstutionByVillage_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutionInter.createInstutionByVillage(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createInstutionByVillage("[{\"institutionName\":\"Sub Centre\"}]")); + } + + @Test + @DisplayName("getInstutionByVillage should answer the institutions under the village") + void getInstutionByVillage_shouldAnswerInstitutionsUnderVillage() { + ArrayList stored = new ArrayList<>(List.of(institution(31, "Sub Centre"))); + when(m_InstitutionInter.getInstutionByVillage(4001, 29, 301, 401, 501)).thenReturn(stored); + + assertSuccessContaining(controller.getInstutionByVillage("{\"providerServiceMapID\":4001,\"stateID\":29," + + "\"districtID\":301,\"blockID\":401,\"villageID\":501}"), "Sub Centre"); + } + + @Test + @DisplayName("getInstutionByVillage should answer an error envelope when the lookup fails") + void getInstutionByVillage_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutionInter.getInstutionByVillage(any(), any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getInstutionByVillage("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("createInstitutionByFile should answer the summary the service reports for the upload") + void createInstitutionByFile_shouldAnswerUploadSummary() { + when(m_InstitutionInter.createInstitutionByFile(any(JsonObject.class))).thenReturn("12 institutions created"); + + assertSuccessContaining(controller.createInstitutionByFile("{\"fileName\":\"institutions.xlsx\"}"), + "12 institutions created"); + } + + @Test + @DisplayName("createInstitutionByFile should answer an error envelope for a payload that is not an object") + void createInstitutionByFile_shouldAnswerErrorEnvelopeForNonObjectPayload() { + assertGenericFailure(controller.createInstitutionByFile("\"just a string\"")); + } + + @Test + @DisplayName("getInstuteSubDirectory should answer the sub-directories under the directory") + void getInstuteSubDirectory_shouldAnswerSubDirectories() { + ArrayList stored = new ArrayList<>(List.of(subDirectory(41, "Government"))); + when(m_InstitutesubdirectoryInter.getInstutesubDirectory(11, 4001)).thenReturn(stored); + + assertSuccessContaining( + controller.getInstuteSubDirectory("{\"instituteDirectoryID\":11,\"providerServiceMapId\":4001}"), + "Government"); + } + + @Test + @DisplayName("getInstuteSubDirectory should answer an error envelope when the lookup fails") + void getInstuteSubDirectory_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutesubdirectoryInter.getInstutesubDirectory(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getInstuteSubDirectory("{\"instituteDirectoryID\":11}")); + } + + @Test + @DisplayName("createInstuteSubDirectory should answer the sub-directories the service stored") + void createInstuteSubDirectory_shouldAnswerStoredSubDirectories() { + ArrayList stored = new ArrayList<>(List.of(subDirectory(41, "Government"))); + when(m_InstitutesubdirectoryInter.CreateInstutesubDirectory(anyList())).thenReturn(stored); + + assertSuccessContaining( + controller.createInstuteSubDirectory("[{\"instituteSubDirectoryName\":\"Government\"}]"), + "Government"); + } + + @Test + @DisplayName("createInstuteSubDirectory should answer an error envelope when the store fails") + void createInstuteSubDirectory_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutesubdirectoryInter.CreateInstutesubDirectory(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createInstuteSubDirectory("[{\"instituteSubDirectoryName\":\"Government\"}]")); + } + + @Test + @DisplayName("editInstuteSubDirectory should copy the edited fields onto the stored sub-directory") + void editInstuteSubDirectory_shouldCopyEditedFields() { + M_Institutesubdirectory stored = subDirectory(41, "old name"); + when(m_InstitutesubdirectoryInter.editInstutesubDirectory(41)).thenReturn(stored); + when(m_InstitutesubdirectoryInter.saveEditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editInstuteSubDirectory("{\"instituteSubDirectoryID\":41," + + "\"instituteSubDirectoryName\":\"Government\",\"instituteSubDirectoryDesc\":\"State run\"," + + "\"modifiedBy\":\"admin\"}"), "Government"); + assertEquals("State run", stored.getInstituteSubDirectoryDesc()); + } + + @Test + @DisplayName("editInstuteSubDirectory should answer an error envelope for a sub-directory that does not exist") + void editInstuteSubDirectory_shouldAnswerErrorEnvelopeForUnknownSubDirectory() { + when(m_InstitutesubdirectoryInter.editInstutesubDirectory(41)).thenReturn(null); + + assertCodeException(controller.editInstuteSubDirectory("{\"instituteSubDirectoryID\":41}")); + } + + @Test + @DisplayName("deleteInstuteSubDirectory should mark the sub-directory deleted") + void deleteInstuteSubDirectory_shouldMarkSubDirectoryDeleted() { + M_Institutesubdirectory stored = subDirectory(41, "Government"); + when(m_InstitutesubdirectoryInter.editInstutesubDirectory(41)).thenReturn(stored); + when(m_InstitutesubdirectoryInter.saveEditedData(stored)).thenReturn(stored); + + assertSuccessContaining( + controller.deleteInstuteSubDirectory("{\"instituteSubDirectoryID\":41,\"deleted\":true}"), + "Government"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteInstuteSubDirectory should answer an error envelope for a sub-directory that does not exist") + void deleteInstuteSubDirectory_shouldAnswerErrorEnvelopeForUnknownSubDirectory() { + when(m_InstitutesubdirectoryInter.editInstutesubDirectory(41)).thenReturn(null); + + assertCodeException(controller.deleteInstuteSubDirectory("{\"instituteSubDirectoryID\":41,\"deleted\":true}")); + } + + @Test + @DisplayName("createInstuteSubDirectoryMaping should answer the mappings the service stored") + void createInstuteSubDirectoryMaping_shouldAnswerStoredMappings() { + ArrayList stored = + new ArrayList<>(List.of(directoryMapping(51, 31))); + when(m_InstitutedirectorymappingInter.createInstituteDirectoryData(anyList())).thenReturn(stored); + + assertSuccessContaining( + controller.createInstuteSubDirectoryMaping("[{\"institutionID\":31,\"instituteDirectoryID\":11}]"), + "51"); + } + + @Test + @DisplayName("createInstuteSubDirectoryMaping should answer an error envelope when the store fails") + void createInstuteSubDirectoryMaping_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutedirectorymappingInter.createInstituteDirectoryData(anyList())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createInstuteSubDirectoryMaping("[{\"institutionID\":31}]")); + } + + @Test + @DisplayName("deleteInstuteSubDirectoryMaping should mark the mapping deleted") + void deleteInstuteSubDirectoryMaping_shouldMarkMappingDeleted() { + M_Institutedirectorymapping stored = directoryMapping(51, 31); + when(m_InstitutedirectorymappingInter.deleteInstituteDirectoryData(51)).thenReturn(stored); + when(m_InstitutedirectorymappingInter.setdeletedData(stored)).thenReturn(stored); + + assertSuccessContaining( + controller.deleteInstuteSubDirectoryMaping("{\"instituteDirMapID\":51,\"deleted\":true}"), + "51"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteInstuteSubDirectoryMaping should answer an error envelope for a mapping that does not exist") + void deleteInstuteSubDirectoryMaping_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(m_InstitutedirectorymappingInter.deleteInstituteDirectoryData(51)).thenReturn(null); + + assertCodeException( + controller.deleteInstuteSubDirectoryMaping("{\"instituteDirMapID\":51,\"deleted\":true}")); + } + + @Test + @DisplayName("getInstuteSubDirectoryMaping should answer the mappings under the sub-directory") + void getInstuteSubDirectoryMaping_shouldAnswerMappings() { + ArrayList stored = + new ArrayList<>(List.of(directoryMapping(51, 31))); + when(m_InstitutedirectorymappingInter.getInstituteDirectoryData(41)).thenReturn(stored); + + assertSuccessContaining(controller.getInstuteSubDirectoryMaping("{\"instituteSubDirectoryID\":41}"), "51"); + } + + @Test + @DisplayName("getInstuteSubDirectoryMaping should answer an error envelope when the lookup fails") + void getInstuteSubDirectoryMaping_shouldAnswerErrorEnvelopeOnFailure() { + when(m_InstitutedirectorymappingInter.getInstituteDirectoryData(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getInstuteSubDirectoryMaping("{\"instituteSubDirectoryID\":41}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardProviderControllerTest.java b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardProviderControllerTest.java new file mode 100644 index 0000000..51e7a6e --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/provideronboard/ProviderOnBoardProviderControllerTest.java @@ -0,0 +1,379 @@ +/* +* 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.admin.controller.provideronboard; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.admin.data.provideronboard.M_ProviderServiceMapping; +import com.iemr.admin.data.provideronboard.M_ServiceMaster; +import com.iemr.admin.data.provideronboard.M_UserservicerolemappingForRole; +import com.iemr.admin.data.provideronboard.ServiceProvider_Model; +import com.iemr.admin.data.provideronboard.V_Showprovideradmin; + +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.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The provider endpoints onboard a service provider, map it onto the states and + * services it serves, and keep its provider-admin users attached to it. + */ +@DisplayName("ProviderOnBoardController provider Test Suite") +class ProviderOnBoardProviderControllerTest extends ProviderOnBoardFixture { + + private static final String ONBOARD_REQUEST = "{\"serviceProviderName\":\"Piramal Swasthya\"," + + "\"statusID\":1,\"createdBy\":\"admin\",\"providerAdminDetails\":[{\"userName\":\"pa.user\"}]," + + "\"stateAndServiceMapList\":[{\"stateId\":\"29\",\"services\":[\"1\",\"3\"]}]}"; + + private static ServiceProvider_Model provider(Integer id, String name) { + ServiceProvider_Model provider = new ServiceProvider_Model(); + provider.setServiceProviderId(id); + provider.setServiceProviderName(name); + return provider; + } + + @Test + @DisplayName("providerCreationAndMapping should report success once the admin role is created") + void providerCreationAndMapping_shouldReportSuccess() { + M_ProviderServiceMapping mapping = new M_ProviderServiceMapping(); + mapping.setProviderServiceMapID(4001); + when(serviceProvider_ServiceImpl.createProvider(any(java.util.Set.class))).thenReturn(77); + when(serviceProvider_ServiceImpl.mapProviderStateService(any())).thenReturn(List.of(mapping)); + when(iemrUserServiceImpl.createUser(any(), anyString())).thenReturn(3117); + when(iemrUserServiceImpl.createUserServiceRoleMapping(anyList(), anyInt(), anyString())).thenReturn(1); + + String response = controller.providerCreationAndMapping(ONBOARD_REQUEST); + + assertSuccessContaining(response, "true"); + } + + @Test + @DisplayName("providerCreationAndMapping should report failure when the admin role cannot be created") + void providerCreationAndMapping_shouldReportRoleCreationFailure() { + M_ProviderServiceMapping mapping = new M_ProviderServiceMapping(); + mapping.setProviderServiceMapID(4001); + when(serviceProvider_ServiceImpl.createProvider(any(java.util.Set.class))).thenReturn(77); + when(serviceProvider_ServiceImpl.mapProviderStateService(any())).thenReturn(List.of(mapping)); + when(iemrUserServiceImpl.createUser(any(), anyString())).thenReturn(3117); + when(iemrUserServiceImpl.createUserServiceRoleMapping(anyList(), anyInt(), anyString())).thenReturn(0); + + assertSuccessContaining(controller.providerCreationAndMapping(ONBOARD_REQUEST), "false"); + } + + @Test + @DisplayName("providerCreationAndMapping should report failure when nothing was mapped for the provider") + void providerCreationAndMapping_shouldReportFailureWhenNothingMapped() { + when(serviceProvider_ServiceImpl.createProvider(any(java.util.Set.class))).thenReturn(77); + when(serviceProvider_ServiceImpl.mapProviderStateService(any())).thenReturn(new ArrayList<>()); + when(iemrUserServiceImpl.createUser(any(), anyString())).thenReturn(3117); + + assertSuccessContaining(controller.providerCreationAndMapping(ONBOARD_REQUEST), "false"); + } + + @Test + @DisplayName("providerCreationAndMapping should report failure when the provider itself is not created") + void providerCreationAndMapping_shouldReportFailureWhenProviderNotCreated() { + when(serviceProvider_ServiceImpl.createProvider(any(java.util.Set.class))).thenReturn(0); + + assertSuccessContaining(controller.providerCreationAndMapping(ONBOARD_REQUEST), "false"); + } + + @Test + @DisplayName("providerCreationAndMapping should answer an error envelope when the service fails") + void providerCreationAndMapping_shouldAnswerErrorEnvelopeOnFailure() { + when(serviceProvider_ServiceImpl.createProvider(any(java.util.Set.class))) + .thenThrow(new IllegalStateException("provider store is unavailable")); + + assertGenericFailure(controller.providerCreationAndMapping(ONBOARD_REQUEST)); + } + + @Test + @DisplayName("updateProvider should answer the provider the service saved") + void updateProvider_shouldAnswerSavedProvider() { + when(serviceProvider_ServiceImpl.getProviderData(77)).thenReturn(provider(77, "old name")); + when(serviceProvider_ServiceImpl.upDateProviderDetails(any())) + .thenReturn(provider(77, "Piramal Swasthya")); + + assertSuccessContaining(controller.updateProvider("{\"serviceProviderId\":77," + + "\"serviceProviderName\":\"Piramal Swasthya\"}"), "Piramal Swasthya"); + } + + @Test + @DisplayName("updateProvider should answer an error envelope for a provider that does not exist") + void updateProvider_shouldAnswerErrorEnvelopeForUnknownProvider() { + when(serviceProvider_ServiceImpl.getProviderData(77)).thenReturn(null); + + assertCodeException(controller.updateProvider("{\"serviceProviderId\":77}")); + } + + @Test + @DisplayName("getServiceLine should answer the service lines the master holds") + void getServiceLine_shouldAnswerServiceLines() { + M_ServiceMaster serviceMaster = new M_ServiceMaster(); + serviceMaster.setServiceID(1); + serviceMaster.setServiceName("Tele Medicine"); + when(m_ServiceMasterInter.getAllServiceLine()).thenReturn(List.of(serviceMaster)); + + assertSuccessContaining(controller.getServiceLine("{}"), "Tele Medicine"); + } + + @Test + @DisplayName("getServiceLine should answer an error envelope when the master cannot be read") + void getServiceLine_shouldAnswerErrorEnvelopeOnFailure() { + when(m_ServiceMasterInter.getAllServiceLine()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getServiceLine("{}")); + } + + @Test + @DisplayName("getAllProviderName should answer every provider the service holds") + void getAllProviderName_shouldAnswerEveryProvider() { + ArrayList providers = new ArrayList<>(List.of(provider(77, "Piramal Swasthya"))); + when(serviceProvider_ServiceImpl.getAllProviderName()).thenReturn(providers); + + assertSuccessContaining(controller.getAllProviderName("{}"), "Piramal Swasthya"); + } + + @Test + @DisplayName("getAllProviderName should answer an error envelope when the lookup fails") + void getAllProviderName_shouldAnswerErrorEnvelopeOnFailure() { + when(serviceProvider_ServiceImpl.getAllProviderName()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllProviderName("{}")); + } + + @Test + @DisplayName("getProviderName should report that a name is already taken") + void getProviderName_shouldReportNameTaken() { + when(serviceProvider_ServiceImpl.getProviderName("Piramal Swasthya")).thenReturn("Piramal Swasthya"); + + assertSuccessContaining(controller.getProviderName("{\"serviceProviderName\":\"Piramal Swasthya\"}"), + "provider_name_exists"); + } + + @Test + @DisplayName("getProviderName should report that a name is still free") + void getProviderName_shouldReportNameFree() { + when(serviceProvider_ServiceImpl.getProviderName("Piramal Swasthya")).thenReturn(null); + + assertSuccessContaining(controller.getProviderName("{\"serviceProviderName\":\"Piramal Swasthya\"}"), + "provider_name_doesnt_exist"); + } + + @Test + @DisplayName("getProviderName should answer an error envelope when the check fails") + void getProviderName_shouldAnswerErrorEnvelopeOnFailure() { + when(serviceProvider_ServiceImpl.getProviderName(anyString())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getProviderName("{\"serviceProviderName\":\"Piramal Swasthya\"}")); + } + + @Test + @DisplayName("getProviderId should answer the mapping the service resolves") + void getProviderId_shouldAnswerResolvedMapping() { + M_ProviderServiceMapping mapping = new M_ProviderServiceMapping(); + mapping.setProviderServiceMapID(4001); + mapping.setServiceProviderID(77); + when(serviceProvider_ServiceImpl.getProviderserviceMapId(4001)).thenReturn(mapping); + + assertSuccessContaining(controller.getProviderId("{\"providerServiceMapID\":4001}"), "4001"); + } + + @Test + @DisplayName("getProviderId should answer an error envelope for a mapping that does not exist") + void getProviderId_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(serviceProvider_ServiceImpl.getProviderserviceMapId(4001)).thenReturn(null); + + assertCodeException(controller.getProviderId("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("createProvider should answer the providers the service stored") + void createProvider_shouldAnswerStoredProviders() { + ArrayList stored = new ArrayList<>(List.of(provider(77, "Piramal Swasthya"))); + when(serviceProvider_ServiceImpl.createProvider1(anyList())).thenReturn(stored); + + assertSuccessContaining(controller.createProvider("[{\"serviceProviderName\":\"Piramal Swasthya\"}]"), + "Piramal Swasthya"); + } + + @Test + @DisplayName("createProvider should refuse an empty batch rather than store nothing quietly") + void createProvider_shouldRefuseEmptyBatch() { + assertGenericFailure(controller.createProvider("[]")); + } + + @Test + @DisplayName("providerUpdate should answer the provider the service saved") + void providerUpdate_shouldAnswerSavedProvider() { + when(serviceProvider_ServiceImpl.getProviderData(77)).thenReturn(provider(77, "old name")); + when(serviceProvider_ServiceImpl.upDateProviderDetails(any())) + .thenReturn(provider(77, "Piramal Swasthya")); + + assertSuccessContaining(controller.providerUpdate("{\"serviceProviderId\":77," + + "\"serviceProviderName\":\"Piramal Swasthya\",\"modifiedBy\":\"admin\"}"), "Piramal Swasthya"); + } + + @Test + @DisplayName("providerUpdate should answer an error envelope for a provider that does not exist") + void providerUpdate_shouldAnswerErrorEnvelopeForUnknownProvider() { + when(serviceProvider_ServiceImpl.getProviderData(77)).thenReturn(null); + + assertCodeException(controller.providerUpdate("{\"serviceProviderId\":77}")); + } + + @Test + @DisplayName("providerDelete should mark the provider deleted and answer what was saved") + void providerDelete_shouldMarkProviderDeleted() { + ServiceProvider_Model stored = provider(77, "Piramal Swasthya"); + when(serviceProvider_ServiceImpl.getProviderData(77)).thenReturn(stored); + when(serviceProvider_ServiceImpl.upDateProviderDetails(stored)).thenReturn(stored); + + assertSuccessContaining(controller.providerDelete("{\"serviceProviderId\":77,\"deleted\":true}"), + "Piramal Swasthya"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("providerDelete should answer an error envelope for a provider that does not exist") + void providerDelete_shouldAnswerErrorEnvelopeForUnknownProvider() { + when(serviceProvider_ServiceImpl.getProviderData(77)).thenReturn(null); + + assertCodeException(controller.providerDelete("{\"serviceProviderId\":77,\"deleted\":true}")); + } + + @Test + @DisplayName("updateProviderAdmin should answer the provider the service saved") + void updateProviderAdmin_shouldAnswerSavedProvider() { + when(serviceProvider_ServiceImpl.getProviderData(77)).thenReturn(provider(77, "old name")); + when(serviceProvider_ServiceImpl.upDateProviderDetails(any())) + .thenReturn(provider(77, "Piramal Swasthya")); + + assertSuccessContaining(controller.updateProviderAdmin("{\"serviceProviderId\":77," + + "\"serviceProviderName\":\"Piramal Swasthya\"}"), "Piramal Swasthya"); + } + + @Test + @DisplayName("updateProviderAdmin should answer an error envelope when the provider cannot be read") + void updateProviderAdmin_shouldAnswerErrorEnvelopeOnFailure() { + when(serviceProvider_ServiceImpl.getProviderData(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.updateProviderAdmin("{\"serviceProviderId\":77}")); + } + + @Test + @DisplayName("mappingProviderAdmintoProvider should map the admin onto every mapping it is given") + void mappingProviderAdmintoProvider_shouldMapEveryMapping() { + M_UserservicerolemappingForRole mapped = new M_UserservicerolemappingForRole(); + mapped.setuSRMappingID(9001); + mapped.setUserID(3117); + ArrayList stored = new ArrayList<>(List.of(mapped)); + when(serviceProvider_ServiceImpl.AddUserRole(anyList())).thenReturn(stored); + + String response = controller.mappingProviderAdmintoProvider( + "[{\"userID\":3117,\"createdBy\":\"admin\",\"serviceProviderMapID1\":[4001,4002]}]"); + + assertSuccessContaining(response, "9001"); + verify(serviceProvider_ServiceImpl).AddUserRole(anyList()); + } + + @Test + @DisplayName("mappingProviderAdmintoProvider should answer an error envelope when no mappings are named") + void mappingProviderAdmintoProvider_shouldAnswerErrorEnvelopeWithoutMappings() { + assertCodeException(controller.mappingProviderAdmintoProvider("[{\"userID\":3117}]")); + } + + @Test + @DisplayName("editMappingProviderAdmintoProvider should answer the mapping the service saved") + void editMappingProviderAdmintoProvider_shouldAnswerSavedMapping() { + M_UserservicerolemappingForRole stored = new M_UserservicerolemappingForRole(); + stored.setuSRMappingID(9001); + when(serviceProvider_ServiceImpl.getPADataForEdit(9001)).thenReturn(stored); + when(serviceProvider_ServiceImpl.insertEditedData(stored)).thenReturn(stored); + + String response = controller.editMappingProviderAdmintoProvider( + "{\"uSRMappingID\":9001,\"providerServiceMapID\":4002,\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "9001"); + assertEquals(4002, stored.getProviderServiceMapID()); + } + + @Test + @DisplayName("editMappingProviderAdmintoProvider should answer an error envelope for an unknown mapping") + void editMappingProviderAdmintoProvider_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(serviceProvider_ServiceImpl.getPADataForEdit(9001)).thenReturn(null); + + assertCodeException(controller.editMappingProviderAdmintoProvider("{\"uSRMappingID\":9001}")); + } + + @Test + @DisplayName("deleteMappingProviderAdmintoProvider should mark the mapping deleted") + void deleteMappingProviderAdmintoProvider_shouldMarkMappingDeleted() { + M_UserservicerolemappingForRole stored = new M_UserservicerolemappingForRole(); + stored.setuSRMappingID(9001); + when(serviceProvider_ServiceImpl.getPADataForEdit(9001)).thenReturn(stored); + when(serviceProvider_ServiceImpl.insertEditedData(stored)).thenReturn(stored); + + assertSuccessContaining( + controller.deleteMappingProviderAdmintoProvider("{\"uSRMappingID\":9001,\"deleted\":true}"), "9001"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteMappingProviderAdmintoProvider should answer an error envelope for an unknown mapping") + void deleteMappingProviderAdmintoProvider_shouldAnswerErrorEnvelopeForUnknownMapping() { + when(serviceProvider_ServiceImpl.getPADataForEdit(9001)).thenReturn(null); + + assertCodeException(controller.deleteMappingProviderAdmintoProvider("{\"uSRMappingID\":9001}")); + } + + @Test + @DisplayName("getMappingProviderAdmintoProvider should answer the provider admins on record") + void getMappingProviderAdmintoProvider_shouldAnswerProviderAdmins() { + V_Showprovideradmin admin = new V_Showprovideradmin(); + admin.setuSRMappingID(9001); + admin.setFirstName("Asha"); + ArrayList admins = new ArrayList<>(List.of(admin)); + when(serviceProvider_ServiceImpl.getProviderAdmins()).thenReturn(admins); + + assertSuccessContaining(controller.getMappingProviderAdmintoProvider("{}"), "Asha"); + } + + @Test + @DisplayName("getMappingProviderAdmintoProvider should answer an error envelope when the lookup fails") + void getMappingProviderAdmintoProvider_shouldAnswerErrorEnvelopeOnFailure() { + when(serviceProvider_ServiceImpl.getProviderAdmins()).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getMappingProviderAdmintoProvider("{}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/questionnaire/QuestionnaireControllerTest.java b/src/test/java/com/iemr/admin/controller/questionnaire/QuestionnaireControllerTest.java new file mode 100644 index 0000000..e0c63ac --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/questionnaire/QuestionnaireControllerTest.java @@ -0,0 +1,181 @@ +/* +* 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.admin.controller.questionnaire; + +import org.json.JSONObject; +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.admin.service.questionnaire.QuestionnaireServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * The questionnaire screen keeps the feedback questions a provider asks: adding + * them, listing them, editing them and retiring them. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("QuestionnaireController Test Suite") +class QuestionnaireControllerTest { + + private static final String REQUEST = "{\"providerServiceMapID\":4001,\"questionID\":501}"; + + @Mock + private QuestionnaireServiceImpl questionnaireService; + + @InjectMocks + private QuestionnaireController controller; + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static String errorMessageOf(String response) { + return new JSONObject(response).getString("errorMessage"); + } + + @Test + @DisplayName("saveQuestionnaire should confirm the questions it stored") + void save_shouldConfirmStoredQuestions() throws Exception { + when(questionnaireService.SaveQuestionnaire(anyString())) + .thenReturn("Questionnaire Data Saved Successfully"); + + String response = controller.saveQuestionnaire(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Questionnaire Data Saved Successfully"), response); + } + + @Test + @DisplayName("saveQuestionnaire should report the failure when nothing came back from the service") + void save_shouldReportMissingAnswer() throws Exception { + when(questionnaireService.SaveQuestionnaire(anyString())).thenReturn(null); + + assertEquals("error in saving Questionnaire data", errorMessageOf(controller.saveQuestionnaire(REQUEST))); + } + + @Test + @DisplayName("saveQuestionnaire should report the failure when the questions cannot be stored") + void save_shouldReportStorageFailure() throws Exception { + when(questionnaireService.SaveQuestionnaire(anyString())) + .thenThrow(new Exception("error in saving Questionnaire data")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.saveQuestionnaire(REQUEST))); + } + + @Test + @DisplayName("getQuestionnaireList should answer the questions the provider asks") + void getList_shouldAnswerProvidersQuestions() { + when(questionnaireService.getQuestionnaireList(anyString())) + .thenReturn("[{\"question\":\"Was the visit useful?\"}]"); + + String response = controller.getQuestionnaireList(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Was the visit useful?"), response); + } + + @Test + @DisplayName("getQuestionnaireList should report the failure when nothing came back from the service") + void getList_shouldReportMissingAnswer() { + when(questionnaireService.getQuestionnaireList(anyString())).thenReturn(null); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getQuestionnaireList(REQUEST))); + } + + @Test + @DisplayName("getQuestionnaireList should report the failure when the list cannot be answered") + void getList_shouldReportLookupFailure() { + when(questionnaireService.getQuestionnaireList(anyString())) + .thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getQuestionnaireList(REQUEST))); + } + + @Test + @DisplayName("deleteQuestionnaire should confirm the question it retired") + void delete_shouldConfirmRetiredQuestion() { + when(questionnaireService.deleteQuestionnaire(anyString())) + .thenReturn("Questionnaire Deleted Successfully"); + + String response = controller.deleteQuestionnaire(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Questionnaire Deleted Successfully"), response); + } + + @Test + @DisplayName("deleteQuestionnaire should report the failure when no question was retired") + void delete_shouldReportNothingRetired() { + when(questionnaireService.deleteQuestionnaire(anyString())).thenReturn(null); + + assertEquals("error occured while deleting question.........", + errorMessageOf(controller.deleteQuestionnaire(REQUEST))); + } + + @Test + @DisplayName("deleteQuestionnaire should report the failure when the retirement cannot be recorded") + void delete_shouldReportStorageFailure() { + when(questionnaireService.deleteQuestionnaire(anyString())).thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.deleteQuestionnaire(REQUEST))); + } + + @Test + @DisplayName("editQuestionnaire should confirm the change it recorded") + void edit_shouldConfirmRecordedChange() { + when(questionnaireService.editQuestionnaire(anyString())).thenReturn("Questionnaire Updated Successfully"); + + String response = controller.editQuestionnaire(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Questionnaire Updated Successfully"), response); + } + + @Test + @DisplayName("editQuestionnaire should report the failure when nothing came back from the service") + void edit_shouldReportMissingAnswer() { + when(questionnaireService.editQuestionnaire(anyString())).thenReturn(null); + + assertEquals("error occured while editing question.........", + errorMessageOf(controller.editQuestionnaire(REQUEST))); + } + + @Test + @DisplayName("editQuestionnaire should report the failure when the change cannot be recorded") + void edit_shouldReportStorageFailure() { + when(questionnaireService.editQuestionnaire(anyString())).thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.editQuestionnaire(REQUEST))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/rolemaster/RoleMasterControllerTest.java b/src/test/java/com/iemr/admin/controller/rolemaster/RoleMasterControllerTest.java new file mode 100644 index 0000000..6db52fa --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/rolemaster/RoleMasterControllerTest.java @@ -0,0 +1,464 @@ +/* +* 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.admin.controller.rolemaster; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.rolemaster.M_Screen; +import com.iemr.admin.data.rolemaster.M_UserservicerolemappingForRoleProviderAdmin; +import com.iemr.admin.data.rolemaster.RoleMaster; +import com.iemr.admin.data.rolemaster.RoleScreenMapping; +import com.iemr.admin.data.rolemaster.StateServiceMapping; +import com.iemr.admin.repository.rolemaster.RoleScreenMappingRepo; +import com.iemr.admin.service.rolemaster.Role_MasterInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The role master endpoints define what each role may see, so a role saved + * without its screens leaves its holders locked out of their own work. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("RoleMasterController Test Suite") +class RoleMasterControllerTest { + + private static final Integer PROVIDER_ID = 77; + private static final Integer PSM_ID = 4001; + + @Mock + private RoleScreenMappingRepo roleScreenMappingRepo; + + @Mock + private Role_MasterInter roleMasterInter; + + @InjectMocks + private RoleMasterController controller; + + private static RoleMaster role(Integer id, String name) { + RoleMaster role = new RoleMaster(); + role.setRoleID(id); + role.setRoleName(name); + role.setProviderServiceMapID(PSM_ID); + role.setCreatedBy("admin"); + return role; + } + + private static StateServiceMapping stateMapping(Integer psmId) { + StateServiceMapping mapping = new StateServiceMapping(); + mapping.setProviderServiceMapID(psmId); + mapping.setServiceProviderID(PROVIDER_ID); + return mapping; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + private static void assertCodeException(String response) { + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(response), response); + } + + @Test + @DisplayName("searchRole should answer the states the provider serves") + void searchRole_shouldAnswerServedStates() { + when(roleMasterInter.getStateByServiceProviderId(PROVIDER_ID)) + .thenReturn(new ArrayList<>(List.of(stateMapping(PSM_ID)))); + + assertSuccessContaining(controller.searchRole("{\"serviceProviderID\":77}"), "4001"); + } + + @Test + @DisplayName("searchRole should answer an error envelope when the lookup fails") + void searchRole_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getStateByServiceProviderId(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchRole("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getService should answer the service lines the provider runs in the state") + void getService_shouldAnswerServiceLinesInState() { + when(roleMasterInter.getServiceByServiceProviderIdAndStateId(PROVIDER_ID, 29)) + .thenReturn(new ArrayList<>(List.of(stateMapping(PSM_ID)))); + + assertSuccessContaining(controller.getService("{\"serviceProviderID\":77,\"stateID\":29}"), "4001"); + } + + @Test + @DisplayName("getService should answer an error envelope when the lookup fails") + void getService_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getServiceByServiceProviderIdAndStateId(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getService("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getServiceByProviderId should answer the service lines the user is mapped to") + void getServiceByProviderId_shouldAnswerMappedServiceLines() { + M_UserservicerolemappingForRoleProviderAdmin mapping = + new M_UserservicerolemappingForRoleProviderAdmin(); + mapping.setuSRMappingID(9001); + when(roleMasterInter.getServiceByServiceProviderIds(3117)) + .thenReturn(new ArrayList<>(List.of(mapping))); + + assertSuccessContaining(controller.getServiceByProviderId("{\"userID\":3117}"), "9001"); + } + + @Test + @DisplayName("getServiceByProviderId should answer an error envelope when the lookup fails") + void getServiceByProviderId_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getServiceByServiceProviderIds(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getServiceByProviderId("{\"userID\":3117}")); + } + + @Test + @DisplayName("getStateByProviderIdAndServiceID should pass the national flag through to the service") + void getStateByProviderIdAndServiceID_shouldPassNationalFlagThrough() { + M_UserservicerolemappingForRoleProviderAdmin mapping = + new M_UserservicerolemappingForRoleProviderAdmin(); + mapping.setuSRMappingID(9001); + when(roleMasterInter.getStateByServiceProviderIdAndServiceLines(3117, 3, Boolean.TRUE)) + .thenReturn(new ArrayList<>(List.of(mapping))); + + assertSuccessContaining(controller.getStateByProviderIdAndServiceID( + "{\"userID\":3117,\"serviceID\":3,\"isNational\":true}"), "9001"); + verify(roleMasterInter).getStateByServiceProviderIdAndServiceLines(3117, 3, Boolean.TRUE); + } + + @Test + @DisplayName("getStateByProviderIdAndServiceID should answer an error envelope when the lookup fails") + void getStateByProviderIdAndServiceID_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getStateByServiceProviderIdAndServiceLines(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getStateByProviderIdAndServiceID("{\"userID\":3117}")); + } + + @Test + @DisplayName("getAllRole should resolve the mapping before reading its roles") + void getAllRole_shouldResolveMappingFirst() { + when(roleMasterInter.getAllByMapId(PROVIDER_ID, 29, 3, Boolean.FALSE)) + .thenReturn(new ArrayList<>(List.of(stateMapping(PSM_ID)))); + when(roleMasterInter.getProStateServRoles(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(role(11, "Counsellor")))); + + assertSuccessContaining(controller.getAllRole("{\"serviceProviderID\":77,\"stateID\":29," + + "\"serviceID\":3,\"isNational\":false}"), "Counsellor"); + } + + @Test + @DisplayName("getAllRole should fall back to no mapping when the provider has none in the state") + void getAllRole_shouldFallBackWithoutMapping() { + when(roleMasterInter.getAllByMapId(any(), any(), any(), any())).thenReturn(new ArrayList<>()); + when(roleMasterInter.getProStateServRoles(0)).thenReturn(new ArrayList<>()); + + controller.getAllRole("{\"serviceProviderID\":77}"); + + verify(roleMasterInter).getProStateServRoles(0); + } + + @Test + @DisplayName("getAllRole should answer an error envelope when the lookup fails") + void getAllRole_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getAllByMapId(any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllRole("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getAllRoleNew should read the roles straight off the mapping the caller names") + void getAllRoleNew_shouldReadRolesFromNamedMapping() { + when(roleMasterInter.getProStateServRoles(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(role(11, "Counsellor")))); + + assertSuccessContaining(controller.getAllRoleNew("{\"providerServiceMapID\":4001}"), "Counsellor"); + } + + @Test + @DisplayName("getAllRoleNew should answer an error envelope when the lookup fails") + void getAllRoleNew_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getProStateServRoles(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllRoleNew("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("getAllRoles should answer the roles the newer query resolves") + void getAllRoles_shouldAnswerResolvedRoles() { + when(roleMasterInter.getProStateServRolesV1(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(role(11, "Counsellor")))); + + assertSuccessContaining(controller.getAllRoles("{\"providerServiceMapID\":4001}"), "Counsellor"); + } + + @Test + @DisplayName("getAllRoles should answer an error envelope when the lookup fails") + void getAllRoles_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getProStateServRolesV1(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllRoles("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("saveRole should map each screen the request names onto the role it stored") + void saveRole_shouldMapScreensOntoStoredRole() { + when(roleMasterInter.addRole(anyList())).thenReturn(List.of(role(11, "Counsellor"))); + + String response = controller.saveRole("[{\"roleName\":\"Counsellor\",\"providerServiceMapID\":4001," + + "\"createdBy\":\"admin\",\"screenID\":[21,22]}]"); + + assertSuccessContaining(response, "Counsellor"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(roleScreenMappingRepo).saveAll(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertEquals(11, captor.getValue().get(0).getRoleID()); + assertEquals(21, captor.getValue().get(0).getScreenID()); + } + + @Test + @DisplayName("saveRole should leave the screens unmapped when the counts do not line up") + void saveRole_shouldLeaveScreensUnmappedOnMismatch() { + when(roleMasterInter.addRole(anyList())) + .thenReturn(List.of(role(11, "Counsellor"), role(12, "Supervisor"))); + + controller.saveRole("[{\"roleName\":\"Counsellor\",\"screenID\":[21]}]"); + + verify(roleScreenMappingRepo, never()).saveAll(anyList()); + } + + @Test + @DisplayName("saveRole should answer an error envelope when the store fails") + void saveRole_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.addRole(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.saveRole("[{\"roleName\":\"Counsellor\"}]")); + } + + @Test + @DisplayName("editRole should copy the edits onto the stored role and remap its screen") + void editRole_shouldCopyEditsAndRemapScreen() { + RoleMaster stored = role(11, "old name"); + when(roleMasterInter.getRoleByRoleId(11)).thenReturn(stored); + when(roleMasterInter.modifydata(stored)).thenReturn(stored); + when(roleMasterInter.settingScreenId(31, 21)).thenReturn("screen mapped"); + + String response = controller.editRole("{\"roleID\":11,\"roleName\":\"Counsellor\"," + + "\"roleDesc\":\"Handles counselling calls\",\"sRSMappingID\":31,\"screenID\":21}"); + + assertSuccessContaining(response, "screen mapped"); + assertEquals("Counsellor", stored.getRoleName()); + assertEquals("Handles counselling calls", stored.getRoleDesc()); + } + + @Test + @DisplayName("editRole should answer an error envelope for a role that does not exist") + void editRole_shouldAnswerErrorEnvelopeForUnknownRole() { + when(roleMasterInter.getRoleByRoleId(11)).thenReturn(null); + + assertCodeException(controller.editRole("{\"roleID\":11}")); + } + + @Test + @DisplayName("deleteRole should mark the role deleted and answer the outcome") + void deleteRole_shouldMarkRoleDeleted() { + RoleMaster stored = role(11, "Counsellor"); + when(roleMasterInter.getRoleByRoleId(11)).thenReturn(stored); + when(roleMasterInter.deletedata(stored)).thenReturn("role deleted"); + + assertSuccessContaining(controller.deleteRole("{\"roleID\":11,\"deleted\":true}"), "role deleted"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteRole should answer an error envelope for a role that does not exist") + void deleteRole_shouldAnswerErrorEnvelopeForUnknownRole() { + when(roleMasterInter.getRoleByRoleId(11)).thenReturn(null); + + assertCodeException(controller.deleteRole("{\"roleID\":11,\"deleted\":true}")); + } + + @Test + @DisplayName("searchFeature should answer the screens the service line offers") + void searchFeature_shouldAnswerServiceScreens() { + M_Screen screen = new M_Screen(); + screen.setScreenID(21); + screen.setScreenName("Call handling"); + when(roleMasterInter.getAllFeature(3)).thenReturn(new ArrayList<>(List.of(screen))); + + assertSuccessContaining(controller.searchFeature("{\"serviceID\":3}"), "Call handling"); + } + + @Test + @DisplayName("searchFeature should answer an error envelope when the lookup fails") + void searchFeature_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getAllFeature(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchFeature("{\"serviceID\":3}")); + } + + @Test + @DisplayName("deleteFeature should retire the role the screen mapping points at") + void deleteFeature_shouldRetireMappedRole() { + RoleMaster stored = role(11, "Counsellor"); + when(roleMasterInter.getRoleByRoleId(11)).thenReturn(stored); + when(roleMasterInter.deletedata(stored)).thenReturn("role deleted"); + + assertSuccessContaining(controller.deleteFeature("{\"roleID\":11,\"screenID\":21}"), "role deleted"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteFeature should answer an error envelope for a role that does not exist") + void deleteFeature_shouldAnswerErrorEnvelopeForUnknownRole() { + when(roleMasterInter.getRoleByRoleId(11)).thenReturn(null); + + assertCodeException(controller.deleteFeature("{\"roleID\":11}")); + } + + @Test + @DisplayName("getAllRole1 should resolve the mapping before reading its roles") + void getAllRole1_shouldResolveMappingFirst() { + when(roleMasterInter.getAllByMapId(PROVIDER_ID, 29, 3, Boolean.FALSE)) + .thenReturn(new ArrayList<>(List.of(stateMapping(PSM_ID)))); + when(roleMasterInter.getProStateServRoles1(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(role(11, "Counsellor")))); + + assertSuccessContaining(controller.getAllRole1("{\"serviceProviderID\":77,\"stateID\":29," + + "\"serviceID\":3,\"isNational\":false}"), "Counsellor"); + } + + @Test + @DisplayName("getAllRole1 should answer an error envelope when the lookup fails") + void getAllRole1_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getAllByMapId(any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllRole1("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("editRolefeature should answer the screen mappings the service stored") + void editRolefeature_shouldAnswerStoredMappings() { + RoleScreenMapping mapping = new RoleScreenMapping(); + mapping.setsRSMappingID(31); + when(roleMasterInter.mapfeature(anyList())).thenReturn(List.of(mapping)); + + assertSuccessContaining(controller.editRolefeature("[{\"roleID\":11,\"screenID\":21}]"), "31"); + } + + @Test + @DisplayName("editRolefeature should answer an error envelope when the mapping fails") + void editRolefeature_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.mapfeature(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.editRolefeature("[{\"roleID\":11}]")); + } + + @Test + @DisplayName("searchRoleTM should answer the telemedicine roles under the mapping") + void searchRoleTM_shouldAnswerTelemedicineRoles() { + when(roleMasterInter.getRoleMasterTM(PSM_ID)).thenReturn(List.of(role(11, "TC Specialist"))); + + assertSuccessContaining(controller.searchRoleTM("{\"providerServiceMapID\":4001}"), "TC Specialist"); + } + + @Test + @DisplayName("searchRoleTM should answer an error envelope when the lookup fails") + void searchRoleTM_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getRoleMasterTM(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.searchRoleTM("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("getAllRoleActive should answer only the roles still in use") + void getAllRoleActive_shouldAnswerActiveRoles() { + when(roleMasterInter.getProStateServRolesActive(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(role(11, "Counsellor")))); + + assertSuccessContaining(controller.getAllRoleActive("{\"providerServiceMapID\":4001}"), "Counsellor"); + } + + @Test + @DisplayName("getAllRoleActive should answer an error envelope when the lookup fails") + void getAllRoleActive_shouldAnswerErrorEnvelopeOnFailure() { + when(roleMasterInter.getProStateServRolesActive(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllRoleActive("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("configWrapUptime should answer the role the service saved") + void configWrapUptime_shouldAnswerSavedRole() throws Exception { + RoleMaster stored = role(11, "Counsellor"); + when(roleMasterInter.configWrapUpTime(any())).thenReturn(stored); + + assertSuccessContaining(controller.configWrapUptime(role(11, "Counsellor")), "Counsellor"); + } + + @Test + @DisplayName("configWrapUptime should answer an error envelope when the change is refused") + void configWrapUptime_shouldAnswerErrorEnvelopeWhenRefused() throws Exception { + when(roleMasterInter.configWrapUpTime(any())) + .thenThrow(new IllegalStateException("wrap up time must be positive")); + + assertGenericFailure(controller.configWrapUptime(role(11, "Counsellor"))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/servicePoint/ServicePointControllerTest.java b/src/test/java/com/iemr/admin/controller/servicePoint/ServicePointControllerTest.java new file mode 100644 index 0000000..f6eb25d --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/servicePoint/ServicePointControllerTest.java @@ -0,0 +1,274 @@ +/* +* 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.admin.controller.servicePoint; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.locationmaster.DistrictBranchMapping; +import com.iemr.admin.data.servicePoint.M_Servicepoint; +import com.iemr.admin.data.servicePoint.M_Servicepointvillagemap; +import com.iemr.admin.service.servicePoint.ServicePointServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The service point endpoints keep the points a mobile unit halts at and the + * villages each point covers. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ServicePointController Test Suite") +class ServicePointControllerTest { + + private static final Integer POINT_ID = 71; + + @Mock + private ServicePointServiceImpl ServicePointServiceImpl; + + @InjectMocks + private ServicePointController controller; + + private static M_Servicepoint point(Integer id, String name) { + return new M_Servicepoint(id, name, "Weekly halt", "Main Road", 4001, Boolean.FALSE, 1, "India", + 29, "Karnataka", 301, "Bengaluru Urban", 401, "North block", 501, "Hosur", null, 3, + "Mobile Medical Unit", 31, "Hosur parking"); + } + + private static M_Servicepointvillagemap villageMap(Integer id) { + return new M_Servicepointvillagemap(id, 29, "Karnataka", 301, "Bengaluru Urban", 31, "Hosur parking", + 71, "Hosur halt", 501, "Hosur", 4001, Boolean.FALSE); + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("saveServicePoint should answer the service points the service stored") + void saveServicePoint_shouldAnswerStoredPoints() throws Exception { + when(ServicePointServiceImpl.saveServicePoint(anyList())) + .thenReturn(new ArrayList<>(List.of(point(POINT_ID, "Hosur halt")))); + + assertSuccessContaining( + controller.saveServicePoint("{\"servicePoints\":[{\"servicePointName\":\"Hosur halt\"}]}"), + "Hosur halt"); + } + + @Test + @DisplayName("saveServicePoint should answer an error envelope when the store fails") + void saveServicePoint_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(ServicePointServiceImpl.saveServicePoint(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.saveServicePoint("{\"servicePoints\":[{}]}")); + } + + @Test + @DisplayName("getServicePoints should answer the points matching the location filters") + void getServicePoints_shouldAnswerMatchingPoints() throws Exception { + when(ServicePointServiceImpl.getAvailableServicePoints(29, 301, 31, 77)) + .thenReturn(new ArrayList<>(List.of(point(POINT_ID, "Hosur halt")))); + + assertSuccessContaining(controller.getServicePoints("{\"stateID\":29,\"districtID\":301," + + "\"parkingPlaceID\":31,\"serviceProviderID\":77}"), "Hosur halt"); + } + + @Test + @DisplayName("getServicePoints should answer an error envelope when the lookup fails") + void getServicePoints_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(ServicePointServiceImpl.getAvailableServicePoints(any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getServicePoints("{\"stateID\":29}")); + } + + @Test + @DisplayName("deleteServicePoint should report whether the point actually changed") + void deleteServicePoint_shouldReportOutcome() throws Exception { + when(ServicePointServiceImpl.updateServicePointStatus(any())).thenReturn(1); + assertSuccessContaining(controller.deleteServicePoint("{\"servicePointID\":71,\"deleted\":true}"), + "status updated successfully"); + + when(ServicePointServiceImpl.updateServicePointStatus(any())).thenReturn(0); + assertSuccessContaining(controller.deleteServicePoint("{\"servicePointID\":71,\"deleted\":true}"), + "Failed to update the status"); + } + + @Test + @DisplayName("deleteServicePoint should answer an error envelope when the change fails") + void deleteServicePoint_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(ServicePointServiceImpl.updateServicePointStatus(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.deleteServicePoint("{\"servicePointID\":71,\"deleted\":true}")); + } + + @Test + @DisplayName("editServicePoint should copy the edits onto the stored point") + void editServicePoint_shouldCopyEdits() throws Exception { + M_Servicepoint stored = point(POINT_ID, "old name"); + when(ServicePointServiceImpl.getdataForEditServicePointStatus(POINT_ID)).thenReturn(stored); + when(ServicePointServiceImpl.saveeditedData(stored)).thenReturn(stored); + + String response = controller.editServicePoint("{\"servicePointID\":71," + + "\"servicePointName\":\"Hosur halt\",\"servicePointDesc\":\"Weekly halt\"," + + "\"districtID\":301,\"districtBlockID\":401,\"servicePointHQAddress\":\"Main Road\"," + + "\"providerServiceMapID\":4001,\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "Hosur halt"); + assertEquals("Weekly halt", stored.getServicePointDesc()); + assertEquals("Main Road", stored.getServicePointHQAddress()); + } + + @Test + @DisplayName("editServicePoint should answer an error envelope for a point that does not exist") + void editServicePoint_shouldAnswerErrorEnvelopeForUnknownPoint() throws Exception { + when(ServicePointServiceImpl.getdataForEditServicePointStatus(POINT_ID)).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, + statusCodeOf(controller.editServicePoint("{\"servicePointID\":71}"))); + } + + @Test + @DisplayName("saveServicePointVillageMap should answer the village maps the service stored") + void saveVillageMap_shouldAnswerStoredMaps() throws Exception { + when(ServicePointServiceImpl.saveServicePointVillageMap(anyList())) + .thenReturn(new ArrayList<>(List.of(villageMap(9001)))); + + assertSuccessContaining(controller.saveServicePointVillageMap( + "{\"servicePointVillageMaps\":[{\"servicePointID\":71,\"districtBranchID\":501}]}"), "9001"); + } + + @Test + @DisplayName("saveServicePointVillageMap should answer an error envelope when the store fails") + void saveVillageMap_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(ServicePointServiceImpl.saveServicePointVillageMap(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.saveServicePointVillageMap("{\"servicePointVillageMaps\":[{}]}")); + } + + @Test + @DisplayName("getServicePointVillageMaps should answer the maps matching the location filters") + void getVillageMaps_shouldAnswerMatchingMaps() throws Exception { + when(ServicePointServiceImpl.getAvailableServicePointVillageMaps(29, 301, 31, POINT_ID, 77)) + .thenReturn(new ArrayList<>(List.of(villageMap(9001)))); + + assertSuccessContaining(controller.getServicePointVillageMaps("{\"stateID\":29,\"districtID\":301," + + "\"parkingPlaceID\":31,\"servicePointID\":71,\"serviceProviderID\":77}"), "9001"); + } + + @Test + @DisplayName("getServicePointVillageMaps should answer an error envelope when the lookup fails") + void getVillageMaps_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(ServicePointServiceImpl.getAvailableServicePointVillageMaps(any(), any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getServicePointVillageMaps("{\"stateID\":29}")); + } + + @Test + @DisplayName("deleteServicePointVillageMap should report whether the map actually changed") + void deleteVillageMap_shouldReportOutcome() throws Exception { + when(ServicePointServiceImpl.updateServicePointVillageMapStatus(any(M_Servicepointvillagemap.class))) + .thenReturn(1); + assertSuccessContaining( + controller.deleteServicePointVillageMap("{\"servicePointVillageMapID\":9001,\"deleted\":true}"), + "status updated successfully"); + + when(ServicePointServiceImpl.updateServicePointVillageMapStatus(any(M_Servicepointvillagemap.class))) + .thenReturn(0); + assertSuccessContaining( + controller.deleteServicePointVillageMap("{\"servicePointVillageMapID\":9001,\"deleted\":true}"), + "Failed to update the status"); + } + + @Test + @DisplayName("editServicePointVillageMap should copy the edits onto the stored map") + void editVillageMap_shouldCopyEdits() throws Exception { + M_Servicepointvillagemap stored = villageMap(9001); + when(ServicePointServiceImpl.updateServicePointVillageMapStatus(9001)).thenReturn(stored); + when(ServicePointServiceImpl.saveEditedData(stored)).thenReturn(stored); + + String response = controller.editServicePointVillageMap("{\"servicePointVillageMapID\":9001," + + "\"servicePointID\":72,\"districtBranchID\":502,\"providerServiceMapID\":4001," + + "\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "9001"); + assertEquals(72, stored.getServicePointID()); + assertEquals(502, stored.getDistrictBranchID()); + } + + @Test + @DisplayName("editServicePointVillageMap should answer an error envelope for a map that does not exist") + void editVillageMap_shouldAnswerErrorEnvelopeForUnknownMap() throws Exception { + when(ServicePointServiceImpl.updateServicePointVillageMapStatus(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, + statusCodeOf(controller.editServicePointVillageMap("{\"servicePointVillageMapID\":9001}"))); + } + + @Test + @DisplayName("unmappedvillages should answer the villages no service point covers yet") + void unmappedvillages_shouldAnswerUncoveredVillages() throws Exception { + when(ServicePointServiceImpl.getunmappedvillages(4001, 401)) + .thenReturn(List.of(new DistrictBranchMapping())); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(controller.unmappedvillages( + "{\"providerServiceMapID\":4001,\"districtBlockID\":401}"))); + } + + @Test + @DisplayName("unmappedvillages should answer an error envelope when the lookup fails") + void unmappedvillages_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(ServicePointServiceImpl.getunmappedvillages(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.unmappedvillages("{\"providerServiceMapID\":4001}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/snomedMapping/SnomedMappingControllerTest.java b/src/test/java/com/iemr/admin/controller/snomedMapping/SnomedMappingControllerTest.java new file mode 100644 index 0000000..75cb0c4 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/snomedMapping/SnomedMappingControllerTest.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.admin.controller.snomedMapping; + +import org.json.JSONObject; +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.google.gson.JsonObject; +import com.iemr.admin.service.snomedMapping.SnomedService; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyString; +import static org.mockito.Mockito.when; + +/** + * The snomed screen maps entries of the clinical masters onto SNOMED codes and + * reports back in the master's own words why a mapping was refused. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("SnomedMappingController Test Suite") +class SnomedMappingControllerTest { + + private static final String REQUEST = "{\"masterType\":\"Family History\",\"masterID\":12," + + "\"sctCode\":\"73211009\",\"modifiedBy\":\"admin\"}"; + + @Mock + private SnomedService snomedService; + + @InjectMocks + private SnomedMappingController controller; + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static String errorMessageOf(String response) { + return new JSONObject(response).getString("errorMessage"); + } + + @Test + @DisplayName("editSnomedMaster should confirm the mapping it recorded") + void edit_shouldConfirmRecordedMapping() { + when(snomedService.editSnomedMappingData(any(JsonObject.class), anyString())).thenReturn("Data Updated"); + + String response = controller.editSnomedMaster(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Data Updated successfully"), response); + } + + @Test + @DisplayName("editSnomedMaster should pass on the master type refusal in the service's own words") + void edit_shouldPassOnMasterTypeRefusal() { + when(snomedService.editSnomedMappingData(any(JsonObject.class), anyString())) + .thenReturn("Invalid Master Type"); + + String response = controller.editSnomedMaster(REQUEST); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response)); + assertEquals("Invalid Master Type", errorMessageOf(response)); + } + + @Test + @DisplayName("editSnomedMaster should report a general refusal when the mapping was not recorded") + void edit_shouldReportGeneralRefusal() { + when(snomedService.editSnomedMappingData(any(JsonObject.class), anyString())).thenReturn(null); + + String response = controller.editSnomedMaster(REQUEST); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response)); + assertEquals("Unable to update data", errorMessageOf(response)); + } + + @Test + @DisplayName("editSnomedMaster should report the failure when the request cannot be read") + void edit_shouldReportUnreadableRequest() { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.editSnomedMaster("{not json"))); + } + + @Test + @DisplayName("saveSnomedMaster should confirm the mappings it stored") + void save_shouldConfirmStoredMappings() { + when(snomedService.saveSnomedMappingData(any(JsonObject.class), anyString())).thenReturn("Data Saved"); + + String response = controller.saveSnomedMaster(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Data Saved successfully"), response); + } + + @Test + @DisplayName("saveSnomedMaster should pass on the master type refusal in the service's own words") + void save_shouldPassOnMasterTypeRefusal() { + when(snomedService.saveSnomedMappingData(any(JsonObject.class), anyString())) + .thenReturn("Invalid Master Type"); + + assertEquals("Invalid Master Type", errorMessageOf(controller.saveSnomedMaster(REQUEST))); + } + + @Test + @DisplayName("saveSnomedMaster should report a general refusal when nothing was stored") + void save_shouldReportGeneralRefusal() { + when(snomedService.saveSnomedMappingData(any(JsonObject.class), anyString())).thenReturn(null); + + assertEquals("Unable to Save data", errorMessageOf(controller.saveSnomedMaster(REQUEST))); + } + + @Test + @DisplayName("saveSnomedMaster should report the failure when the request cannot be read") + void save_shouldReportUnreadableRequest() { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.saveSnomedMaster("{not json"))); + } + + @Test + @DisplayName("fetchSnomedWorklist should answer the master the request asked for") + void fetch_shouldAnswerRequestedMaster() { + when(snomedService.fetchSnomedMaster(any(JsonObject.class))).thenReturn("[{\"masterName\":\"Diabetes\"}]"); + + String response = controller.fetchSnomedWorklist(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Diabetes"), response); + } + + @Test + @DisplayName("fetchSnomedWorklist should report the failure when the worklist cannot be answered") + void fetch_shouldReportLookupFailure() { + when(snomedService.fetchSnomedMaster(any(JsonObject.class))).thenReturn(null); + + assertEquals("error in fetching worklist data", errorMessageOf(controller.fetchSnomedWorklist(REQUEST))); + } + + @Test + @DisplayName("fetchSnomedWorklist should report the failure when the request cannot be read") + void fetch_shouldReportUnreadableRequest() { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.fetchSnomedWorklist("{not json"))); + } + + @Test + @DisplayName("updateStatus should confirm the change it recorded") + void updateStatus_shouldConfirmRecordedChange() { + when(snomedService.updateStatus(anyString())).thenReturn("Data updated successfully"); + + String response = controller.updateStatus(REQUEST); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Data updated successfully"), response); + } + + @Test + @DisplayName("updateStatus should report the failure when nothing came back from the service") + void updateStatus_shouldReportMissingAnswer() { + when(snomedService.updateStatus(anyString())).thenReturn(null); + + assertEquals("error in updating data", errorMessageOf(controller.updateStatus(REQUEST))); + } + + @Test + @DisplayName("updateStatus should report the failure when the change cannot be recorded") + void updateStatus_shouldReportStorageFailure() { + when(snomedService.updateStatus(anyString())).thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.updateStatus(REQUEST))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/stockEntry/StockEntryControllerTest.java b/src/test/java/com/iemr/admin/controller/stockEntry/StockEntryControllerTest.java new file mode 100644 index 0000000..d85c5a3 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/stockEntry/StockEntryControllerTest.java @@ -0,0 +1,145 @@ +/* +* 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.admin.controller.stockEntry; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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 org.springframework.dao.DataIntegrityViolationException; + +import com.iemr.admin.data.stockExit.ItemStockExit; +import com.iemr.admin.data.stockentry.ItemStockEntry; +import com.iemr.admin.data.stockentry.PhysicalStockEntry; +import com.iemr.admin.service.stockEntry.StockEntryService; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The stock entry screen records what arrives at a store and works out which + * batches an issue should draw on. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("StockEntryController Test Suite") +class StockEntryControllerTest { + + private static final Integer FACILITY_ID = 9001; + + @Mock + private StockEntryService stockEntryService; + + @InjectMocks + private StockEntryController controller; + + private static PhysicalStockEntry arrival() { + PhysicalStockEntry arrival = new PhysicalStockEntry(); + arrival.setPhyEntryID(7001); + arrival.setRefNo("GRN-101"); + return arrival; + } + + private static ItemStockEntry batch() { + ItemStockEntry batch = new ItemStockEntry(); + batch.setItemStockEntryID(1); + batch.setItemID(501); + batch.setFacilityID(FACILITY_ID); + batch.setBatchNo("B-77"); + return batch; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + @Test + @DisplayName("physicalStockEntry should answer the arrival it recorded") + void physicalStockEntry_shouldAnswerRecordedArrival() { + when(stockEntryService.savePhysicalStockEntry(any(PhysicalStockEntry.class))).thenReturn(arrival()); + + assertSuccessContaining(controller.physicalStockEntry(arrival()), "GRN-101"); + } + + @Test + @DisplayName("physicalStockEntry should report the failure when a batch clashes with one on file") + void physicalStockEntry_shouldReportClashingBatch() { + when(stockEntryService.savePhysicalStockEntry(any(PhysicalStockEntry.class))) + .thenThrow(new DataIntegrityViolationException("duplicate batch number")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.physicalStockEntry(arrival()))); + } + + @Test + @DisplayName("getItemBatchForStoreID should answer the batches the store still holds") + void getItemBatch_shouldAnswerHeldBatches() { + when(stockEntryService.getItemBatchForStoreID(any(ItemStockEntry.class))).thenReturn(List.of(batch())); + + assertSuccessContaining(controller.getItemBatchForStoreID(batch()), "B-77"); + } + + @Test + @DisplayName("getItemBatchForStoreID should report the failure when the batches cannot be answered") + void getItemBatch_shouldReportLookupFailure() { + when(stockEntryService.getItemBatchForStoreID(any(ItemStockEntry.class))) + .thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getItemBatchForStoreID(batch()))); + } + + @Test + @DisplayName("allocateStockFromItemID should answer the batches the issue should draw on") + void allocate_shouldAnswerAllocatedBatches() { + when(stockEntryService.getItemStockFromItemID(anyInt(), anyList())).thenReturn(List.of(batch())); + + assertSuccessContaining( + controller.allocateStockFromItemID(FACILITY_ID, new ArrayList()), "B-77"); + } + + @Test + @DisplayName("allocateStockFromItemID should report the failure when the allocation cannot be worked out") + void allocate_shouldReportAllocationFailure() { + when(stockEntryService.getItemStockFromItemID(anyInt(), anyList())) + .thenThrow(new RuntimeException("item is not on the item master")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.allocateStockFromItemID(FACILITY_ID, new ArrayList()))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/stockExit/StockExitControllerTest.java b/src/test/java/com/iemr/admin/controller/stockExit/StockExitControllerTest.java new file mode 100644 index 0000000..d2bea7b --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/stockExit/StockExitControllerTest.java @@ -0,0 +1,99 @@ +/* +* 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.admin.controller.stockExit; + +import org.json.JSONObject; +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.admin.data.stockExit.T_PatientIssue; +import com.iemr.admin.service.stockExit.StockExitService; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The patient issue screen hands drugs out of a store to a patient. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("StockExitController Test Suite") +class StockExitControllerTest { + + @Mock + private StockExitService stockExitService; + + @InjectMocks + private StockExitController controller; + + private static T_PatientIssue issue() { + T_PatientIssue issue = new T_PatientIssue(); + issue.setPatientIssueID(5501); + issue.setFacilityID(9001); + issue.setIssueType("Manual"); + return issue; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + @Test + @DisplayName("patientIssue should confirm the issue it recorded") + void patientIssue_shouldConfirmRecordedIssue() { + when(stockExitService.issuePatientDrugs(any(T_PatientIssue.class))).thenReturn(1); + + String response = controller.patientIssue(issue()); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Successfully Created"), response); + } + + @Test + @DisplayName("patientIssue should say the quantities are wrong when the store cannot cover the issue") + void patientIssue_shouldSayQuantitiesAreWrong() { + when(stockExitService.issuePatientDrugs(any(T_PatientIssue.class))).thenReturn(0); + + String response = controller.patientIssue(issue()); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Error in Quantity"), response); + } + + @Test + @DisplayName("patientIssue should report the failure when the issue cannot be recorded") + void patientIssue_shouldReportStorageFailure() { + when(stockExitService.issuePatientDrugs(any(T_PatientIssue.class))) + .thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.patientIssue(issue()))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/store/StoreControllerTest.java b/src/test/java/com/iemr/admin/controller/store/StoreControllerTest.java new file mode 100644 index 0000000..39f8fac --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/store/StoreControllerTest.java @@ -0,0 +1,453 @@ +/* +* 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.admin.controller.store; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.store.FacilityVillageMapping; +import com.iemr.admin.data.store.M_Facility; +import com.iemr.admin.data.store.M_facilityMap; +import com.iemr.admin.data.store.V_FetchFacility; +import com.iemr.admin.service.store.StoreService; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The store endpoints maintain the facility hierarchy an operator sees on the + * store screens. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("StoreController Test Suite") +class StoreControllerTest { + + private static final Integer FACILITY_ID = 501; + private static final Integer PSM_ID = 4001; + private static final Integer BLOCK_ID = 401; + + @Mock + private StoreService storeService; + + @InjectMocks + private StoreController controller; + + private static M_Facility facility(Integer id, String name) { + M_Facility facility = new M_Facility(); + facility.setFacilityID(id); + facility.setFacilityName(name); + return facility; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + private static void assertCodeException(String response) { + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(response), response); + } + + @Test + @DisplayName("createStore should answer the facilities the service stored") + void createStore_shouldAnswerStoredFacilities() { + when(storeService.addAllMainStore(anyList())).thenReturn(List.of(facility(FACILITY_ID, "PHC North"))); + + assertSuccessContaining(controller.createStore("[{\"facilityName\":\"PHC North\"}]"), "PHC North"); + } + + @Test + @DisplayName("createStore should answer an error envelope when the store fails") + void createStore_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.addAllMainStore(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createStore("[{\"facilityName\":\"PHC North\"}]")); + } + + @Test + @DisplayName("editStore should copy the edited fields onto the stored facility") + void editStore_shouldCopyEditedFields() { + M_Facility stored = facility(FACILITY_ID, "PHC North"); + when(storeService.getMainStore(FACILITY_ID)).thenReturn(stored); + when(storeService.createMainStore(stored)).thenReturn(stored); + + String response = controller.editStore("{\"facilityID\":501,\"facilityDesc\":\"Primary centre\"," + + "\"location\":\"Main Road\",\"physicalLocation\":\"Block A\",\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "PHC North"); + assertEquals("Primary centre", stored.getFacilityDesc()); + assertEquals("Block A", stored.getPhysicalLocation()); + } + + @Test + @DisplayName("editStore should answer an error envelope for a facility that does not exist") + void editStore_shouldAnswerErrorEnvelopeForUnknownFacility() { + when(storeService.getMainStore(FACILITY_ID)).thenReturn(null); + + assertCodeException(controller.editStore("{\"facilityID\":501}")); + } + + @Test + @DisplayName("getAllStore should answer the facilities of the provider") + void getAllStore_shouldAnswerProviderFacilities() { + when(storeService.getAllMainStore(PSM_ID)).thenReturn(List.of(facility(FACILITY_ID, "PHC North"))); + + assertSuccessContaining(controller.getAllStore(PSM_ID), "PHC North"); + } + + @Test + @DisplayName("getAllStore should answer an error envelope when the lookup fails") + void getAllStore_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.getAllMainStore(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllStore(PSM_ID)); + } + + @Test + @DisplayName("getMainFacility should answer the main facilities of the provider") + void getMainFacility_shouldAnswerMainFacilities() { + when(storeService.getMainFacility(PSM_ID, true)) + .thenReturn(new ArrayList<>(List.of(facility(FACILITY_ID, "PHC North")))); + + assertSuccessContaining( + controller.getMainFacility("{\"providerServiceMapID\":4001,\"isMainFacility\":true}"), "PHC North"); + } + + @Test + @DisplayName("getMainFacility should answer an error envelope when the lookup fails") + void getMainFacility_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.getMainFacility(any(), any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getMainFacility("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("getsubFacility should answer the facilities under the main facility") + void getsubFacility_shouldAnswerSubFacilities() { + when(storeService.getMainFacility(PSM_ID, false, 500)) + .thenReturn(new ArrayList<>(List.of(facility(FACILITY_ID, "Sub Centre")))); + + assertSuccessContaining(controller.getsubFacility("{\"providerServiceMapID\":4001," + + "\"isMainFacility\":false,\"mainFacilityID\":500}"), "Sub Centre"); + } + + @Test + @DisplayName("getsubFacility should answer an error envelope when the lookup fails") + void getsubFacility_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.getMainFacility(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getsubFacility("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("deleteStore should answer the facility the service retired") + void deleteStore_shouldAnswerRetiredFacility() throws Exception { + M_Facility request = facility(FACILITY_ID, "PHC North"); + when(storeService.deleteStore(request)).thenReturn(request); + + assertSuccessContaining(controller.deleteStore(request), "PHC North"); + } + + @Test + @DisplayName("deleteStore should report the reason the service refused to retire the facility") + void deleteStore_shouldReportRefusal() throws Exception { + M_Facility request = facility(FACILITY_ID, "PHC North"); + when(storeService.deleteStore(request)).thenThrow(new Exception("Child Stores are still active")); + + String response = controller.deleteStore(request); + + assertGenericFailure(response); + assertTrue(response.contains("Child Stores are still active"), response); + } + + @Test + @DisplayName("mapStore should answer how many mappings the service changed") + void mapStore_shouldAnswerChangedCount() { + when(storeService.mapStore(anyList())).thenReturn(2); + + assertSuccessContaining(controller.mapStore(List.of(new M_facilityMap())), "2"); + } + + @Test + @DisplayName("mapStore should answer an error envelope when the mapping fails") + void mapStore_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.mapStore(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.mapStore(List.of(new M_facilityMap()))); + } + + @Test + @DisplayName("deleteMapStore should answer how many mappings the service released") + void deleteMapStore_shouldAnswerReleasedCount() throws Exception { + M_facilityMap request = new M_facilityMap(); + when(storeService.deleteMapStore(request)).thenReturn(1); + + assertSuccessContaining(controller.deleteMapStore(request), "1"); + } + + @Test + @DisplayName("deleteMapStore should report the reason the service refused to release the mapping") + void deleteMapStore_shouldReportRefusal() throws Exception { + M_facilityMap request = new M_facilityMap(); + when(storeService.deleteMapStore(request)) + .thenThrow(new Exception("Please Unmap van under this Parking Place")); + + assertTrue(controller.deleteMapStore(request).contains("Please Unmap van under this Parking Place")); + } + + @Test + @DisplayName("getMapStore should answer the mapped facilities of the provider") + void getMapStore_shouldAnswerMappedFacilities() { + V_FetchFacility request = new V_FetchFacility(); + request.setProviderServiceMapID(PSM_ID); + V_FetchFacility mapped = new V_FetchFacility(); + mapped.setFacilityName("PHC North"); + when(storeService.getMapStore(request)).thenReturn(List.of(mapped)); + + assertSuccessContaining(controller.getMapStore(request), "PHC North"); + } + + @Test + @DisplayName("getMapStore should answer an error envelope when the lookup fails") + void getMapStore_shouldAnswerErrorEnvelopeOnFailure() { + V_FetchFacility request = new V_FetchFacility(); + when(storeService.getMapStore(request)).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getMapStore(request)); + } + + @Test + @DisplayName("getFacilitiesByBlock should answer the live facilities in the block") + void getFacilitiesByBlock_shouldAnswerLiveFacilities() { + when(storeService.getFacilitiesByBlock(BLOCK_ID)) + .thenReturn(new ArrayList<>(List.of(facility(FACILITY_ID, "PHC North")))); + + assertSuccessContaining(controller.getFacilitiesByBlock("{\"blockID\":401}"), "PHC North"); + } + + @Test + @DisplayName("getFacilitiesByBlock should answer an error envelope when the lookup fails") + void getFacilitiesByBlock_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.getFacilitiesByBlock(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getFacilitiesByBlock("{\"blockID\":401}")); + } + + @Test + @DisplayName("getAllFacilitiesByBlock should answer the retired facilities of the block as well") + void getAllFacilitiesByBlock_shouldAnswerRetiredFacilitiesToo() { + when(storeService.getAllFacilitiesByBlock(BLOCK_ID)) + .thenReturn(new ArrayList<>(List.of(facility(FACILITY_ID, "PHC North")))); + + assertSuccessContaining(controller.getAllFacilitiesByBlock("{\"blockID\":401}"), "PHC North"); + } + + @Test + @DisplayName("getAllFacilitiesByBlock should answer an error envelope when the lookup fails") + void getAllFacilitiesByBlock_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.getAllFacilitiesByBlock(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getAllFacilitiesByBlock("{\"blockID\":401}")); + } + + @Test + @DisplayName("checkStoreCode should report whether the facility code is already taken") + void checkStoreCode_shouldReportWhetherCodeIsTaken() { + when(storeService.checkStoreCode(any())).thenReturn(Boolean.TRUE); + + assertSuccessContaining(controller.checkStoreCode("{\"facilityCode\":\"PHC-1\"}"), "true"); + } + + @Test + @DisplayName("checkStoreCode should answer an error envelope when the check fails") + void checkStoreCode_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.checkStoreCode(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.checkStoreCode("{\"facilityCode\":\"PHC-1\"}")); + } + + @Test + @DisplayName("getFacilitiesByBlockAndLevel should pass every filter the screen sends through") + void getFacilitiesByBlockAndLevel_shouldPassFiltersThrough() { + when(storeService.getFacilitiesByBlockAndLevel(BLOCK_ID, 4, "Rural")) + .thenReturn(new ArrayList<>(List.of(facility(FACILITY_ID, "Sub Centre")))); + + assertSuccessContaining(controller.getFacilitiesByBlockAndLevel( + "{\"blockID\":401,\"levelValue\":4,\"ruralUrban\":\"Rural\"}"), "Sub Centre"); + verify(storeService).getFacilitiesByBlockAndLevel(BLOCK_ID, 4, "Rural"); + } + + @Test + @DisplayName("getFacilitiesByBlockAndLevel should answer an error envelope when the lookup fails") + void getFacilitiesByBlockAndLevel_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.getFacilitiesByBlockAndLevel(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getFacilitiesByBlockAndLevel("{\"blockID\":401}")); + } + + @Test + @DisplayName("createFacilityWithHierarchy should pass the villages and children through to the service") + void createFacilityWithHierarchy_shouldPassHierarchyThrough() { + when(storeService.createFacilityWithHierarchy(any(), anyList(), anyInt(), anyList())) + .thenReturn(facility(FACILITY_ID, "PHC North")); + + String response = controller.createFacilityWithHierarchy("{\"facility\":{\"facilityName\":\"PHC North\"}," + + "\"villageIDs\":[601,602],\"mainVillageID\":601,\"childFacilityIDs\":[502]}"); + + assertSuccessContaining(response, "PHC North"); + verify(storeService).createFacilityWithHierarchy(any(), org.mockito.ArgumentMatchers.eq(List.of(601, 602)), + org.mockito.ArgumentMatchers.eq(601), org.mockito.ArgumentMatchers.eq(List.of(502))); + } + + @Test + @DisplayName("createFacilityWithHierarchy should report the reason the service refused the facility") + void createFacilityWithHierarchy_shouldReportRefusal() { + when(storeService.createFacilityWithHierarchy(any(), any(), any(), any())) + .thenThrow(new RuntimeException("Facility with this name already exists in this block")); + + assertTrue(controller.createFacilityWithHierarchy("{\"facility\":{\"facilityName\":\"PHC North\"}}") + .contains("already exists in this block")); + } + + @Test + @DisplayName("getMappedVillageIDs should answer the villages already spoken for in the block") + void getMappedVillageIDs_shouldAnswerSpokenForVillages() { + when(storeService.getMappedVillageIDs(BLOCK_ID)).thenReturn(List.of(601, 602)); + + assertSuccessContaining(controller.getMappedVillageIDs("{\"blockID\":401}"), "601"); + } + + @Test + @DisplayName("getMappedVillageIDs should answer an error envelope when the lookup fails") + void getMappedVillageIDs_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.getMappedVillageIDs(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getMappedVillageIDs("{\"blockID\":401}")); + } + + @Test + @DisplayName("getVillageMappingsByFacility should answer the villages the facility serves") + void getVillageMappingsByFacility_shouldAnswerServedVillages() { + FacilityVillageMapping mapping = new FacilityVillageMapping(); + mapping.setDistrictBranchID(601); + when(storeService.getVillageMappingsByFacility(FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(mapping))); + + assertSuccessContaining(controller.getVillageMappingsByFacility("{\"facilityID\":501}"), "601"); + } + + @Test + @DisplayName("getVillageMappingsByFacility should answer an error envelope when the lookup fails") + void getVillageMappingsByFacility_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.getVillageMappingsByFacility(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getVillageMappingsByFacility("{\"facilityID\":501}")); + } + + @Test + @DisplayName("getChildFacilitiesByParent should answer the facilities under the parent") + void getChildFacilitiesByParent_shouldAnswerChildren() { + when(storeService.getChildFacilitiesByParent(FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(facility(502, "Sub Centre")))); + + assertSuccessContaining(controller.getChildFacilitiesByParent("{\"facilityID\":501}"), "Sub Centre"); + } + + @Test + @DisplayName("getChildFacilitiesByParent should answer an error envelope when the lookup fails") + void getChildFacilitiesByParent_shouldAnswerErrorEnvelopeOnFailure() { + when(storeService.getChildFacilitiesByParent(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getChildFacilitiesByParent("{\"facilityID\":501}")); + } + + @Test + @DisplayName("updateFacilityWithHierarchy should answer the facility the service saved") + void updateFacilityWithHierarchy_shouldAnswerSavedFacility() { + when(storeService.updateFacilityWithHierarchy(any(), any(), any(), any())) + .thenReturn(facility(FACILITY_ID, "PHC North")); + + assertSuccessContaining(controller.updateFacilityWithHierarchy( + "{\"facility\":{\"facilityID\":501,\"facilityName\":\"PHC North\"}}"), "PHC North"); + } + + @Test + @DisplayName("updateFacilityWithHierarchy should report the reason the service refused the edit") + void updateFacilityWithHierarchy_shouldReportRefusal() { + when(storeService.updateFacilityWithHierarchy(any(), any(), any(), any())) + .thenThrow(new RuntimeException("Facility not found")); + + assertTrue(controller.updateFacilityWithHierarchy("{\"facility\":{\"facilityID\":501}}") + .contains("Facility not found")); + } + + @Test + @DisplayName("deleteFacilityWithHierarchy should answer the facility the service retired") + void deleteFacilityWithHierarchy_shouldAnswerRetiredFacility() throws Exception { + when(storeService.deleteFacilityWithHierarchy(FACILITY_ID, "admin")) + .thenReturn(facility(FACILITY_ID, "PHC North")); + + assertSuccessContaining( + controller.deleteFacilityWithHierarchy("{\"facilityID\":501,\"modifiedBy\":\"admin\"}"), + "PHC North"); + } + + @Test + @DisplayName("deleteFacilityWithHierarchy should report the reason the service refused the retirement") + void deleteFacilityWithHierarchy_shouldReportRefusal() throws Exception { + when(storeService.deleteFacilityWithHierarchy(anyInt(), anyString())) + .thenThrow(new Exception("Facility not found")); + + assertTrue(controller.deleteFacilityWithHierarchy("{\"facilityID\":501,\"modifiedBy\":\"admin\"}") + .contains("Facility not found")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/supplier/SupplierMasterControllerTest.java b/src/test/java/com/iemr/admin/controller/supplier/SupplierMasterControllerTest.java new file mode 100644 index 0000000..e6b8929 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/supplier/SupplierMasterControllerTest.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.admin.controller.supplier; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.supplier.M_Supplier; +import com.iemr.admin.service.supplier.SupplierInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** The supplier master endpoints keep the supplier catalogue an inventory operator picks from. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("SupplierMasterController Test Suite") +class SupplierMasterControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer RECORD_ID = 11; + + @Mock + private SupplierInter supplierInter; + + @InjectMocks + private SupplierMasterController controller; + + private static M_Supplier record(Integer id, String name) { + M_Supplier record = new M_Supplier(); + record.setSupplierID(id); + record.setSupplierName(name); + return record; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("createSupplier should answer the records the service stored") + void create_shouldAnswerStoredRecords() { + when(supplierInter.createSupplier(anyList())).thenReturn(new ArrayList<>(List.of(record(RECORD_ID, "MedSupply")))); + + assertSuccessContaining(controller.createSupplier("[{\"supplierCode\":\"C-1\"}]"), "MedSupply"); + } + + @Test + @DisplayName("createSupplier should answer an error envelope when the store fails") + void create_shouldAnswerErrorEnvelopeOnFailure() { + when(supplierInter.createSupplier(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createSupplier("[{}]")); + } + + @Test + @DisplayName("getSupplier should answer the records of the provider") + void get_shouldAnswerProviderRecords() { + when(supplierInter.getSupplier(PSM_ID)).thenReturn(new ArrayList<>(List.of(record(RECORD_ID, "MedSupply")))); + + assertSuccessContaining(controller.getSupplier("{\"providerServiceMapID\":4001}"), "MedSupply"); + } + + @Test + @DisplayName("getSupplier should answer an error envelope when the lookup fails") + void get_shouldAnswerErrorEnvelopeOnFailure() { + when(supplierInter.getSupplier(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getSupplier("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("editSupplier should copy the edits onto the stored record") + void edit_shouldCopyEdits() { + M_Supplier stored = record(RECORD_ID, "MedSupply"); + when(supplierInter.editSupplier(RECORD_ID)).thenReturn(stored); + when(supplierInter.saveEditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editSupplier("{\"supplierID\":11,\"supplierDesc\":\"Bulk supplier\",\"contactPerson\":\"Asha\",\"email\":\"asha@example.org\",\"modifiedBy\":\"admin\"}"), "MedSupply"); + assertEquals("Bulk supplier", stored.getSupplierDesc()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("editSupplier should answer an error envelope for a record that does not exist") + void edit_shouldAnswerErrorEnvelopeForUnknownRecord() { + when(supplierInter.editSupplier(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.editSupplier("{\"supplierID\":11,\"supplierDesc\":\"Bulk supplier\",\"contactPerson\":\"Asha\",\"email\":\"asha@example.org\",\"modifiedBy\":\"admin\"}"))); + } + + @Test + @DisplayName("deleteSupplier should mark the record deleted") + void delete_shouldMarkRecordDeleted() { + M_Supplier stored = record(RECORD_ID, "MedSupply"); + when(supplierInter.editSupplier(RECORD_ID)).thenReturn(stored); + when(supplierInter.saveEditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteSupplier("{\"supplierID\":11,\"deleted\":true}"), "MedSupply"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteSupplier should answer an error envelope for a record that does not exist") + void delete_shouldAnswerErrorEnvelopeForUnknownRecord() { + when(supplierInter.editSupplier(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.deleteSupplier("{\"supplierID\":11,\"deleted\":true}"))); + } + + @Test + @DisplayName("checkSupplierCode should report whether the code is already taken") + void check_shouldReportWhetherCodeIsTaken() { + when(supplierInter.checkSupplierCode(any())).thenReturn(Boolean.TRUE); + + assertSuccessContaining(controller.checkSupplierCode("{\"supplierCode\":\"C-1\"}"), "true"); + } + + @Test + @DisplayName("checkSupplierCode should answer an error envelope when the check fails") + void check_shouldAnswerErrorEnvelopeOnFailure() { + when(supplierInter.checkSupplierCode(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.checkSupplierCode("{\"supplierCode\":\"C-1\"}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/telemedicine/TeleMedicineControllerTest.java b/src/test/java/com/iemr/admin/controller/telemedicine/TeleMedicineControllerTest.java new file mode 100644 index 0000000..3338ca9 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/telemedicine/TeleMedicineControllerTest.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.admin.controller.telemedicine; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.telemedicine.M_UserTemp; +import com.iemr.admin.data.telemedicine.Specialization; +import com.iemr.admin.data.telemedicine.TMinput; +import com.iemr.admin.data.telemedicine.UserSpecializationMapping; +import com.iemr.admin.service.telemedicine.TMInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The telemedicine screen lists the specialists a provider has and records + * which specialities each of them holds. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("TeleMedicineController Test Suite") +class TeleMedicineControllerTest { + + private static final Integer PROVIDER_ID = 5; + private static final Integer MAP_ID = 8001; + + @Mock + private TMInter tmInter; + + @InjectMocks + private TeleMedicineController controller; + + private static TMinput request() { + TMinput input = new TMinput(); + input.setServiceproviderID(PROVIDER_ID); + input.setScreenName("TM"); + return input; + } + + private static M_UserTemp specialist() { + M_UserTemp user = new M_UserTemp(); + user.setUserID(3117L); + user.setFirstName("Asha"); + user.setUserName("asha.rao"); + return user; + } + + private static Specialization speciality() { + Specialization speciality = new Specialization(); + speciality.setSpecializationID(11); + speciality.setSpecialization("Paediatrics"); + return speciality; + } + + private static UserSpecializationMapping held() { + UserSpecializationMapping mapping = new UserSpecializationMapping(); + mapping.setUserSpecializationMapID(MAP_ID); + mapping.setSpecializationName("Paediatrics"); + mapping.setDeleted(Boolean.FALSE); + mapping.setModifiedBy("admin"); + return mapping; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + @Test + @DisplayName("getUserTM should answer the specialists on the screen asked for") + void getUserTM_shouldAnswerSpecialists() { + when(tmInter.getUser(any())).thenReturn(new ArrayList<>(List.of(specialist()))); + + assertSuccessContaining(controller.getUserTM(request()), "asha.rao"); + } + + @Test + @DisplayName("getUserTM should report the failure when the roster cannot be answered") + void getUserTM_shouldReportLookupFailure() { + when(tmInter.getUser(any())).thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getUserTM(request()))); + } + + @Test + @DisplayName("getSpecialization should answer the specialities on file") + void getSpecialization_shouldAnswerSpecialities() { + when(tmInter.getSpecialization()).thenReturn(new ArrayList<>(List.of(speciality()))); + + assertSuccessContaining(controller.getSpecialization(), "Paediatrics"); + } + + @Test + @DisplayName("getSpecialization should report the failure when the list cannot be answered") + void getSpecialization_shouldReportLookupFailure() { + when(tmInter.getSpecialization()).thenThrow(new RuntimeException("connection reset")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getSpecialization())); + } + + @Test + @DisplayName("getUserSpecialization should answer the specialities held under the provider asked for") + void getUserSpecialization_shouldAnswerHeldSpecialities() { + when(tmInter.getUserSpecialization(PROVIDER_ID)).thenReturn(new ArrayList<>(List.of(held()))); + + assertSuccessContaining(controller.getUserSpecialization(request()), "Paediatrics"); + } + + @Test + @DisplayName("getUserSpecialization should report the failure when the lookup cannot be answered") + void getUserSpecialization_shouldReportLookupFailure() { + when(tmInter.getUserSpecialization(any())).thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getUserSpecialization(request()))); + } + + @Test + @DisplayName("saveUserSpecialization should answer the specialities it recorded") + void saveUserSpecialization_shouldAnswerRecordedSpecialities() { + ArrayList request = new ArrayList<>(List.of(held())); + when(tmInter.saveUserSpecialization(request)).thenReturn(request); + + assertSuccessContaining(controller.saveUserSpecialization(request), "Paediatrics"); + } + + @Test + @DisplayName("saveUserSpecialization should report the failure when the speciality cannot be recorded") + void saveUserSpecialization_shouldReportStorageFailure() { + ArrayList request = new ArrayList<>(List.of(held())); + when(tmInter.saveUserSpecialization(request)).thenThrow(new RuntimeException("already recorded")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.saveUserSpecialization(request))); + } + + @Test + @DisplayName("activating a speciality should answer it with the status the caller asked for") + void activate_shouldAnswerSpecialityWithRequestedStatus() { + UserSpecializationMapping stored = held(); + UserSpecializationMapping request = held(); + request.setDeleted(Boolean.TRUE); + request.setModifiedBy("supervisor"); + when(tmInter.findUserSpecialization(request)).thenReturn(stored); + when(tmInter.saveoneUserSpecialization(stored)).thenReturn(stored); + + assertSuccessContaining(controller.saveUserSpecialization(request), "8001"); + assertEquals(Boolean.TRUE, stored.getDeleted()); + assertEquals("supervisor", stored.getModifiedBy()); + } + + @Test + @DisplayName("activating a speciality should report the failure when it is unknown") + void activate_shouldReportUnknownSpeciality() { + when(tmInter.findUserSpecialization(any())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.saveUserSpecialization(held()))); + } + + @Test + @DisplayName("activating a speciality should report the failure when the change cannot be recorded") + void activate_shouldReportStorageFailure() { + UserSpecializationMapping stored = held(); + when(tmInter.findUserSpecialization(any())).thenReturn(stored); + when(tmInter.saveoneUserSpecialization(stored)).thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.saveUserSpecialization(held()))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/telemedicine/VideoConsultationControllerTest.java b/src/test/java/com/iemr/admin/controller/telemedicine/VideoConsultationControllerTest.java new file mode 100644 index 0000000..5e44a86 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/telemedicine/VideoConsultationControllerTest.java @@ -0,0 +1,199 @@ +/* +* 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.admin.controller.telemedicine; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.telemedicine.M_UserTemp; +import com.iemr.admin.data.telemedicine.UserVideoConsultation; +import com.iemr.admin.data.telemedicine.VideoConsultationDomain; +import com.iemr.admin.service.telemedicine.VideoConsultationInter; +import com.iemr.admin.utils.exception.VideoConsultationException; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * The video consultation screen mints conferencing accounts for clinicians and + * keeps their sign-in details and status up to date. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VideoConsultationController Test Suite") +class VideoConsultationControllerTest { + + private static final Integer PROVIDER_ID = 5; + private static final Long MAP_ID = 8001L; + + @Mock + private VideoConsultationInter videoConsultationInter; + + @InjectMocks + private VideoConsultationController controller; + + private static M_UserTemp clinician() { + M_UserTemp user = new M_UserTemp(); + user.setUserID(3117L); + user.setUserName("asha.rao"); + return user; + } + + private static UserVideoConsultation account() { + UserVideoConsultation account = new UserVideoConsultation(); + account.setUserVideoConsultationMapID(MAP_ID); + account.setUserID(3117L); + account.setVideoConsultationEmailID("asha.rao@example.org"); + account.setVideoConsultationDomain("psmri"); + return account; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + @Test + @DisplayName("getUserTM should answer the clinicians who have no conferencing account yet") + void getUnmapped_shouldAnswerCliniciansWithoutAccount() { + when(videoConsultationInter.getunmappedUser(PROVIDER_ID, 7)).thenReturn(List.of(clinician())); + + assertSuccessContaining(controller.getUserTM(PROVIDER_ID, 7), "asha.rao"); + } + + @Test + @DisplayName("getUserTM should report the failure when the candidates cannot be worked out") + void getUnmapped_shouldReportLookupFailure() { + when(videoConsultationInter.getunmappedUser(any(), any())) + .thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getUserTM(PROVIDER_ID, 7))); + } + + @Test + @DisplayName("createUserTM should answer the conferencing account it opened") + void createUser_shouldAnswerOpenedAccount() throws Exception { + when(videoConsultationInter.createUser(any())).thenReturn(account()); + + assertSuccessContaining(controller.createUserTM(account()), "asha.rao@example.org"); + } + + @Test + @DisplayName("createUserTM should report the platform's reason when the account is refused") + void createUser_shouldReportRemoteRefusal() throws Exception { + when(videoConsultationInter.createUser(any())) + .thenThrow(new VideoConsultationException("email already registered")); + + assertEquals(OutputResponse.VIDEOCONSULTATION_EXCEPTION, statusCodeOf(controller.createUserTM(account()))); + } + + @Test + @DisplayName("editUser should answer the account whose sign-in details it changed") + void editUser_shouldAnswerChangedAccount() throws Exception { + when(videoConsultationInter.editUser(any())).thenReturn(account()); + + assertSuccessContaining(controller.editUser(account()), "8001"); + } + + @Test + @DisplayName("editUser should report the failure when the account is unknown") + void editUser_shouldReportUnknownAccount() throws Exception { + when(videoConsultationInter.editUser(any())).thenThrow(new VideoConsultationException("Invalid MapID")); + + assertEquals(OutputResponse.VIDEOCONSULTATION_EXCEPTION, statusCodeOf(controller.editUser(account()))); + } + + @Test + @DisplayName("deleting an account should answer it with the status the caller asked for") + void deleteUser_shouldAnswerAccountWithRequestedStatus() throws Exception { + UserVideoConsultation retired = account(); + retired.setDeleted(Boolean.TRUE); + when(videoConsultationInter.deleteUser(MAP_ID, Boolean.TRUE, "supervisor")).thenReturn(retired); + + assertSuccessContaining(controller.createUserTM("supervisor", MAP_ID, Boolean.TRUE), "8001"); + } + + @Test + @DisplayName("deleting an account should report the failure when the platform refuses the change") + void deleteUser_shouldReportRemoteRefusal() throws Exception { + when(videoConsultationInter.deleteUser(anyLong(), anyBoolean(), anyString())) + .thenThrow(new VideoConsultationException("account is locked")); + + assertEquals(OutputResponse.VIDEOCONSULTATION_EXCEPTION, + statusCodeOf(controller.createUserTM("supervisor", MAP_ID, Boolean.TRUE))); + } + + @Test + @DisplayName("getmappedUsers should answer the accounts held under the provider asked for") + void getmappedUsers_shouldAnswerAccountsOfProvider() { + when(videoConsultationInter.fetchmappedUser(PROVIDER_ID)).thenReturn(new ArrayList<>(List.of(account()))); + + assertSuccessContaining(controller.getmappedUsers(PROVIDER_ID), "asha.rao@example.org"); + } + + @Test + @DisplayName("getmappedUsers should report the failure when the lookup cannot be answered") + void getmappedUsers_shouldReportLookupFailure() { + when(videoConsultationInter.fetchmappedUser(any())).thenThrow(new RuntimeException("connection reset")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getmappedUsers(PROVIDER_ID))); + } + + @Test + @DisplayName("getdomain should answer the conferencing domains on file") + void getdomain_shouldAnswerDomains() { + VideoConsultationDomain domain = new VideoConsultationDomain(); + domain.setVideoConsultationDomainID(1); + domain.setVideoConsultationDoamin("psmri"); + when(videoConsultationInter.getdomain(PROVIDER_ID)).thenReturn(List.of(domain)); + + assertSuccessContaining(controller.getdomain(PROVIDER_ID), "psmri"); + } + + @Test + @DisplayName("getdomain should report the failure when the domains cannot be answered") + void getdomain_shouldReportLookupFailure() { + when(videoConsultationInter.getdomain(any())).thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getdomain(PROVIDER_ID))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/uom/UomControllerTest.java b/src/test/java/com/iemr/admin/controller/uom/UomControllerTest.java new file mode 100644 index 0000000..5651471 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/uom/UomControllerTest.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.admin.controller.uom; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.uom.M_Uom; +import com.iemr.admin.service.uom.UomInter; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** The unit of measure master endpoints keep the unit of measure catalogue an inventory operator picks from. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("UomController Test Suite") +class UomControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer RECORD_ID = 11; + + @Mock + private UomInter uomInter; + + @InjectMocks + private UomController controller; + + private static M_Uom record(Integer id, String name) { + M_Uom record = new M_Uom(); + record.setuOMID(id); + record.setuOMName(name); + return record; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + @Test + @DisplayName("createUom should answer the records the service stored") + void create_shouldAnswerStoredRecords() { + when(uomInter.createDrugtypeData(anyList())).thenReturn(new ArrayList<>(List.of(record(RECORD_ID, "Tablet")))); + + assertSuccessContaining(controller.createUom("[{\"uOMCode\":\"C-1\"}]"), "Tablet"); + } + + @Test + @DisplayName("createUom should answer an error envelope when the store fails") + void create_shouldAnswerErrorEnvelopeOnFailure() { + when(uomInter.createDrugtypeData(anyList())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.createUom("[{}]")); + } + + @Test + @DisplayName("getUom should answer the records of the provider") + void get_shouldAnswerProviderRecords() { + when(uomInter.createDrugtypeData(PSM_ID)).thenReturn(new ArrayList<>(List.of(record(RECORD_ID, "Tablet")))); + + assertSuccessContaining(controller.getUom("{\"providerServiceMapID\":4001}"), "Tablet"); + } + + @Test + @DisplayName("getUom should answer an error envelope when the lookup fails") + void get_shouldAnswerErrorEnvelopeOnFailure() { + when(uomInter.createDrugtypeData(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getUom("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("editUom should copy the edits onto the stored record") + void edit_shouldCopyEdits() { + M_Uom stored = record(RECORD_ID, "Tablet"); + when(uomInter.editDrugtypeData(RECORD_ID)).thenReturn(stored); + when(uomInter.saveeditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.editUom("{\"uOMID\":11,\"uOMName\":\"Tablet\",\"uOMDesc\":\"One tablet\",\"uOMCode\":\"TAB\",\"status\":\"Active\",\"modifiedBy\":\"admin\"}"), "Tablet"); + assertEquals("One tablet", stored.getuOMDesc()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("editUom should answer an error envelope for a record that does not exist") + void edit_shouldAnswerErrorEnvelopeForUnknownRecord() { + when(uomInter.editDrugtypeData(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.editUom("{\"uOMID\":11,\"uOMName\":\"Tablet\",\"uOMDesc\":\"One tablet\",\"uOMCode\":\"TAB\",\"status\":\"Active\",\"modifiedBy\":\"admin\"}"))); + } + + @Test + @DisplayName("deleteUom should mark the record deleted") + void delete_shouldMarkRecordDeleted() { + M_Uom stored = record(RECORD_ID, "Tablet"); + when(uomInter.editDrugtypeData(RECORD_ID)).thenReturn(stored); + when(uomInter.saveeditedData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.deleteUom("{\"uOMID\":11,\"deleted\":true}"), "Tablet"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteUom should answer an error envelope for a record that does not exist") + void delete_shouldAnswerErrorEnvelopeForUnknownRecord() { + when(uomInter.editDrugtypeData(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.deleteUom("{\"uOMID\":11,\"deleted\":true}"))); + } + + @Test + @DisplayName("checkUomCode should report whether the code is already taken") + void check_shouldReportWhetherCodeIsTaken() { + when(uomInter.checkUomCode(any())).thenReturn(Boolean.TRUE); + + assertSuccessContaining(controller.checkUomCode("{\"uOMCode\":\"C-1\"}"), "true"); + } + + @Test + @DisplayName("checkUomCode should answer an error envelope when the check fails") + void check_shouldAnswerErrorEnvelopeOnFailure() { + when(uomInter.checkUomCode(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.checkUomCode("{\"uOMCode\":\"C-1\"}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/uptsu/FacilityControllerTest.java b/src/test/java/com/iemr/admin/controller/uptsu/FacilityControllerTest.java new file mode 100644 index 0000000..7e58df8 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/uptsu/FacilityControllerTest.java @@ -0,0 +1,155 @@ +/* +* 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.admin.controller.uptsu; + +import java.util.List; + +import org.json.JSONObject; +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.admin.data.uptsu.CDSSMapping; +import com.iemr.admin.data.uptsu.M_FacilityMapping; +import com.iemr.admin.service.uptsu.FacilityService; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The UP TSU endpoints take the facility spreadsheet an operator uploads and the + * CDSS switch that decides whether decision support is on for a provider. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("FacilityController Test Suite") +class FacilityControllerTest { + + private static final Integer PSM_ID = 4001; + private static final String AUTH = "sess-0d5f3a7c"; + + @Mock + private FacilityService uptsuService; + + @InjectMocks + private FacilityController controller; + + private static final String UPLOAD_REQUEST = "{\"createdBy\":\"admin\",\"fileName\":\"facilities.xlsx\"," + + "\"providerServiceMapID\":4001,\"fileExtension\":\"xlsx\",\"fileContent\":\"data:x;base64,AAA=\"}"; + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + @Test + @DisplayName("saveFacilityData should report the upload once the service has stored it") + void saveFacilityData_shouldReportStoredUpload() throws Exception { + when(uptsuService.saveFacility(any())).thenReturn(List.of(new M_FacilityMapping())); + + String response = controller.saveFacilityData(UPLOAD_REQUEST, AUTH); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("saveFacilityData saved successfully"), response); + } + + @Test + @DisplayName("saveFacilityData should refuse an upload the service could not read") + void saveFacilityData_shouldRefuseUnreadableUpload() throws Exception { + when(uptsuService.saveFacility(any())) + .thenThrow(new com.iemr.admin.utils.exception.IEMRException("Error in validating cell")); + + String response = controller.saveFacilityData(UPLOAD_REQUEST, AUTH); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + assertTrue(response.contains("Invalid Request"), response); + } + + @Test + @DisplayName("saveFacilityData should refuse a request body that is not a valid upload at all") + void saveFacilityData_shouldRefuseMalformedRequest() { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.saveFacilityData("not json", AUTH))); + } + + @Test + @DisplayName("saveFacilityData should stay at its default when the service stored nothing") + void saveFacilityData_shouldStayAtDefaultWhenNothingStored() throws Exception { + when(uptsuService.saveFacility(any())).thenReturn(List.of()); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.saveFacilityData(UPLOAD_REQUEST, AUTH))); + } + + @Test + @DisplayName("saveCdssDetails should report the configuration once the service has stored it") + void saveCdssDetails_shouldReportStoredConfiguration() { + when(uptsuService.saveCdssDetails(any())).thenReturn(new CDSSMapping()); + + String response = controller.saveCdssDetails("{\"psmId\":4001,\"isCdss\":true}"); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Data saved successfully"), response); + } + + @Test + @DisplayName("saveCdssDetails should report the reason the service refused the configuration") + void saveCdssDetails_shouldReportRefusal() { + when(uptsuService.saveCdssDetails(any())).thenThrow(new IllegalStateException("no connection")); + + String response = controller.saveCdssDetails("{\"psmId\":4001}"); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + assertTrue(response.contains("no connection"), response); + } + + @Test + @DisplayName("saveCdssDetails should refuse a request body it cannot read") + void saveCdssDetails_shouldRefuseMalformedRequest() { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.saveCdssDetails("not json"))); + } + + @Test + @DisplayName("getCdssData should answer the configuration the service publishes") + void getCdssData_shouldAnswerPublishedConfiguration() throws Exception { + when(uptsuService.getCdssData(PSM_ID)).thenReturn("{\"psmId\":4001,\"isCdss\":true}"); + + String response = controller.getCdssData(PSM_ID, AUTH); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("4001"), response); + } + + @Test + @DisplayName("getCdssData should answer an error envelope when the lookup fails") + void getCdssData_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(uptsuService.getCdssData(anyInt())).thenThrow(new IllegalStateException("no connection")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getCdssData(PSM_ID, AUTH))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/userParkingPlaceMap/UserParkingPlaceMapControllerTest.java b/src/test/java/com/iemr/admin/controller/userParkingPlaceMap/UserParkingPlaceMapControllerTest.java new file mode 100644 index 0000000..e9a169b --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/userParkingPlaceMap/UserParkingPlaceMapControllerTest.java @@ -0,0 +1,289 @@ +/* +* 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.admin.controller.userParkingPlaceMap; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.employeemaster.M_User1; +import com.iemr.admin.data.userParkingPlaceMap.M_UserParkingPlaceMap; +import com.iemr.admin.data.userParkingPlaceMap.M_UserVanMapping; +import com.iemr.admin.service.userParkingPlaceMap.UserParkingPlaceMapServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The user parking place endpoints post a field user to the parking place they + * report to, and to the vans they work out of. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("UserParkingPlaceMapController Test Suite") +class UserParkingPlaceMapControllerTest { + + private static final Integer MAP_ID = 9001; + private static final Integer PSM_ID = 4001; + + @Mock + private UserParkingPlaceMapServiceImpl userParkingPlaceMapServiceImpl; + + @InjectMocks + private UserParkingPlaceMapController controller; + + private static M_UserParkingPlaceMap mapping(Integer id) { + return new M_UserParkingPlaceMap(id, 3117, "Asha", "Rao", "asha.rao", 7, 301, 31, "Hosur parking", + PSM_ID, Boolean.FALSE, Boolean.FALSE); + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + private static void assertCodeException(String response) { + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(response), response); + } + + @Test + @DisplayName("saveuserParkingPlaces should answer the postings the service stored") + void save_shouldAnswerStoredPostings() throws Exception { + when(userParkingPlaceMapServiceImpl.saveUserParkingPlaceDetails(anyList())) + .thenReturn(new ArrayList<>(List.of(mapping(MAP_ID)))); + + assertSuccessContaining(controller.saveuserParkingPlaces( + "{\"userParkingPlaceMaps\":[{\"userID\":3117,\"parkingPlaceID\":31}]}"), "Asha"); + } + + @Test + @DisplayName("saveuserParkingPlaces should answer an error envelope when the store fails") + void save_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(userParkingPlaceMapServiceImpl.saveUserParkingPlaceDetails(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.saveuserParkingPlaces("{\"userParkingPlaceMaps\":[{}]}")); + } + + @Test + @DisplayName("getuserParkingPlaces should answer the postings matching the location filters") + void get_shouldAnswerMatchingPostings() throws Exception { + when(userParkingPlaceMapServiceImpl.getUserParkingPlaceMappings(77, 29, 301, 31, 7)) + .thenReturn(new ArrayList<>(List.of(mapping(MAP_ID)))); + + assertSuccessContaining(controller.getuserParkingPlaces("{\"serviceProviderID\":77,\"stateID\":29," + + "\"districtID\":301,\"parkingPlaceID\":31,\"m_user\":{\"designationID\":7}}"), "Asha"); + } + + @Test + @DisplayName("getuserParkingPlaces should answer an error envelope when the request names no user detail") + void get_shouldAnswerErrorEnvelopeWithoutUserDetail() throws Exception { + assertCodeException(controller.getuserParkingPlaces("{\"serviceProviderID\":77}")); + } + + @Test + @DisplayName("getuserParkingPlacesDesiganation should narrow the postings to the designation") + void getByDesignation_shouldNarrowToDesignation() throws Exception { + when(userParkingPlaceMapServiceImpl.getUserParkingPlaceMappings1(PSM_ID, 301, 31, 7)) + .thenReturn(new ArrayList<>(List.of(mapping(MAP_ID)))); + + assertSuccessContaining(controller.getuserParkingPlacesDesiganation( + "{\"providerServiceMapID\":4001,\"districtID\":301,\"parkingPlaceID\":31," + + "\"designationID\":7}"), "Asha"); + } + + @Test + @DisplayName("getuserParkingPlacesDesiganation should answer an error envelope when the lookup fails") + void getByDesignation_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(userParkingPlaceMapServiceImpl.getUserParkingPlaceMappings1(any(), any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getuserParkingPlacesDesiganation("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("deleteuserParkingPlaceDetails should report whether the posting actually changed") + void deleteDetails_shouldReportOutcome() throws Exception { + when(userParkingPlaceMapServiceImpl.updateUserParkingPlaceMapStatus(any())).thenReturn(1); + assertSuccessContaining( + controller.deleteuserParkingPlaceDetails("{\"userParkingPlaceMapID\":9001,\"deleted\":true}"), + "status updated successfully"); + + when(userParkingPlaceMapServiceImpl.updateUserParkingPlaceMapStatus(any())).thenReturn(0); + assertSuccessContaining( + controller.deleteuserParkingPlaceDetails("{\"userParkingPlaceMapID\":9001,\"deleted\":true}"), + "Failed to update the status"); + } + + @Test + @DisplayName("deleteuserParkingPlaceDetails should answer an error envelope when the change fails") + void deleteDetails_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(userParkingPlaceMapServiceImpl.updateUserParkingPlaceMapStatus(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure( + controller.deleteuserParkingPlaceDetails("{\"userParkingPlaceMapID\":9001,\"deleted\":true}")); + } + + @Test + @DisplayName("edituserParkingPlaces should copy the edits and remap the vans") + void edit_shouldCopyEditsAndRemapVans() throws Exception { + M_UserParkingPlaceMap stored = mapping(MAP_ID); + when(userParkingPlaceMapServiceImpl.getUserParkingPlaceDetails(MAP_ID)).thenReturn(stored); + when(userParkingPlaceMapServiceImpl.saveediteddata(any(), anyList())).thenReturn(stored); + + String response = controller.edituserParkingPlaces("{\"userParkingPlaceMapID\":9001," + + "\"parkingPlaceID\":32,\"providerServiceMapID\":4002,\"districtID\":302," + + "\"modifiedBy\":\"admin\",\"uservanmapping\":[{\"vanID\":71}]}"); + + assertSuccessContaining(response, "Asha"); + assertEquals(32, stored.getParkingPlaceID()); + assertEquals(4002, stored.getProviderServiceMapID()); + } + + @Test + @DisplayName("edituserParkingPlaces should answer an error envelope for a posting that does not exist") + void edit_shouldAnswerErrorEnvelopeForUnknownPosting() throws Exception { + when(userParkingPlaceMapServiceImpl.getUserParkingPlaceDetails(anyInt())).thenReturn(null); + + assertCodeException(controller.edituserParkingPlaces("{\"userParkingPlaceMapID\":9001}")); + } + + @Test + @DisplayName("deleteuserParkingPlaces should retire the posting the caller names") + void deletePostings_shouldRetirePosting() throws Exception { + M_UserParkingPlaceMap stored = mapping(MAP_ID); + when(userParkingPlaceMapServiceImpl.getUserParkingPlaceDetails(MAP_ID)).thenReturn(stored); + when(userParkingPlaceMapServiceImpl.saveediteddata(stored)).thenReturn(stored); + + assertSuccessContaining( + controller.deleteuserParkingPlaces("{\"userParkingPlaceMapID\":9001,\"deleted\":true}"), "Asha"); + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("deleteuserParkingPlaces should refuse to reinstate a user already posted elsewhere") + void deletePostings_shouldRefuseReinstatingPostedUser() throws Exception { + M_UserParkingPlaceMap stored = mapping(MAP_ID); + when(userParkingPlaceMapServiceImpl.getUserParkingPlaceDetails(MAP_ID)).thenReturn(stored); + when(userParkingPlaceMapServiceImpl.getuserexist(PSM_ID, 3117)).thenReturn(Boolean.TRUE); + + String response = controller + .deleteuserParkingPlaces("{\"userParkingPlaceMapID\":9001,\"deleted\":false}"); + + assertGenericFailure(response); + assertTrue(response.contains("User already mapped. Cannot activate"), response); + } + + @Test + @DisplayName("deleteuserParkingPlaces should reinstate a user who is posted nowhere else") + void deletePostings_shouldReinstateFreeUser() throws Exception { + M_UserParkingPlaceMap stored = mapping(MAP_ID); + when(userParkingPlaceMapServiceImpl.getUserParkingPlaceDetails(MAP_ID)).thenReturn(stored); + when(userParkingPlaceMapServiceImpl.getuserexist(PSM_ID, 3117)).thenReturn(Boolean.FALSE); + when(userParkingPlaceMapServiceImpl.saveediteddata(stored)).thenReturn(stored); + + controller.deleteuserParkingPlaces("{\"userParkingPlaceMapID\":9001,\"deleted\":false}"); + + assertEquals(Boolean.FALSE, stored.getDeleted()); + } + + @Test + @DisplayName("unmappeduser should answer the users not yet posted anywhere") + void unmappeduser_shouldAnswerUnpostedUsers() throws Exception { + M_User1 user = new M_User1(); + user.setUserID(3117); + user.setFirstName("Asha"); + when(userParkingPlaceMapServiceImpl.getunmappedUser(PSM_ID, 7)).thenReturn(List.of(user)); + + assertSuccessContaining( + controller.unmappeduser("{\"providerServiceMapID\":4001,\"designationID\":7}"), "Asha"); + } + + @Test + @DisplayName("unmappeduser should answer an error envelope when the lookup fails") + void unmappeduser_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(userParkingPlaceMapServiceImpl.getunmappedUser(any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.unmappeduser("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("mappedvan should answer the vans the posting covers") + void mappedvan_shouldAnswerCoveredVans() throws Exception { + M_UserVanMapping vanMapping = new M_UserVanMapping(); + vanMapping.setUserVanMapID(7001); + when(userParkingPlaceMapServiceImpl.getuservanmapping(MAP_ID)).thenReturn(List.of(vanMapping)); + + assertSuccessContaining(controller.mappedvan(MAP_ID), "7001"); + } + + @Test + @DisplayName("mappedvan should answer an error envelope when the lookup fails") + void mappedvan_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(userParkingPlaceMapServiceImpl.getuservanmapping(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.mappedvan(MAP_ID)); + } + + @Test + @DisplayName("deletemappedvan should report the van mapping it released") + void deletemappedvan_shouldReportReleasedMapping() throws Exception { + assertSuccessContaining( + controller.deletemappedvan("{\"userVanMapID\":7001,\"modifiedBy\":\"admin\"}"), "Success"); + verify(userParkingPlaceMapServiceImpl).deleteuservanmapping(any()); + } + + @Test + @DisplayName("deletemappedvan should answer an error envelope when the release fails") + void deletemappedvan_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + org.mockito.Mockito.doThrow(new IllegalStateException("no connection")) + .when(userParkingPlaceMapServiceImpl).deleteuservanmapping(any()); + + assertGenericFailure(controller.deletemappedvan("{\"userVanMapID\":7001}")); + } +} diff --git a/src/test/java/com/iemr/admin/controller/vanMaster/VanMasterControllerTest.java b/src/test/java/com/iemr/admin/controller/vanMaster/VanMasterControllerTest.java new file mode 100644 index 0000000..9a4e8f8 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/vanMaster/VanMasterControllerTest.java @@ -0,0 +1,281 @@ +/* +* 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.admin.controller.vanMaster; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.vanMaster.M_Van; +import com.iemr.admin.data.vanType.M_VanType; +import com.iemr.admin.service.vanMaster.VanMasterServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.when; + +/** + * The van screens keep the mobile unit fleet: what vans exist, what type each + * is, and which parking place or main store each belongs to. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VanMasterController Test Suite") +class VanMasterControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer VAN_ID = 71; + private static final Integer PARKING_PLACE_ID = 31; + + @Mock + private VanMasterServiceImpl vanMasterServiceImpl; + + @InjectMocks + private VanMasterController controller; + + private static M_Van van() { + M_Van van = new M_Van(); + van.setVanID(VAN_ID); + van.setVanName("Mobile unit 7"); + van.setVehicalNo("KA-01-AB-1234"); + van.setParkingPlaceID(PARKING_PLACE_ID); + van.setProviderServiceMapID(PSM_ID); + return van; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + @Test + @DisplayName("saveVanDetails should answer the vans it added to the fleet") + void saveVanDetails_shouldAnswerAddedVans() throws Exception { + when(vanMasterServiceImpl.saveVanDetails(anyList())).thenReturn(new ArrayList<>(List.of(van()))); + + assertSuccessContaining(controller.saveVanDetails( + "{\"vanMaster\":[{\"vanName\":\"Mobile unit 7\",\"vehicalNo\":\"KA-01-AB-1234\"}]}"), + "Mobile unit 7"); + } + + @Test + @DisplayName("saveVanDetails should report the failure when the van cannot be added") + void saveVanDetails_shouldReportStorageFailure() throws Exception { + when(vanMasterServiceImpl.saveVanDetails(anyList())) + .thenThrow(new RuntimeException("vehicle number already on file")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.saveVanDetails("{\"vanMaster\":[{\"vanName\":\"Mobile unit 7\"}]}"))); + } + + @Test + @DisplayName("getServicePoints should answer the vans of the parking place and type asked for") + void getVans_shouldAnswerVansOfParkingPlaceAndType() throws Exception { + when(vanMasterServiceImpl.getAvailableVans(PARKING_PLACE_ID, 2, PSM_ID)) + .thenReturn(new ArrayList<>(List.of(van()))); + + assertSuccessContaining(controller.getServicePoints( + "{\"parkingPlaceID\":31,\"vanTypeID\":2,\"providerServiceMapID\":4001}"), "Mobile unit 7"); + } + + @Test + @DisplayName("getServicePoints should report the failure when the fleet cannot be answered") + void getVans_shouldReportLookupFailure() throws Exception { + when(vanMasterServiceImpl.getAvailableVans(any(), any(), any())) + .thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getServicePoints("{\"providerServiceMapID\":4001}"))); + } + + @Test + @DisplayName("deleteVanDetails should confirm the retirement it recorded") + void deleteVanDetails_shouldConfirmRetirement() throws Exception { + when(vanMasterServiceImpl.updateVanStatus(any(M_Van.class))).thenReturn(1); + + assertSuccessContaining(controller.deleteVanDetails("{\"vanID\":71,\"deleted\":true,\"modifiedBy\":\"admin\"}"), + "status updated successfully"); + } + + @Test + @DisplayName("deleteVanDetails should say so when no van was retired") + void deleteVanDetails_shouldSaySoWhenNothingRetired() throws Exception { + when(vanMasterServiceImpl.updateVanStatus(any(M_Van.class))).thenReturn(0); + + assertSuccessContaining(controller.deleteVanDetails("{\"vanID\":-1,\"deleted\":true}"), + "Failed to update the status"); + } + + @Test + @DisplayName("deleteVanDetails should report the failure when the retirement cannot be recorded") + void deleteVanDetails_shouldReportRetirementFailure() throws Exception { + when(vanMasterServiceImpl.updateVanStatus(any(M_Van.class))).thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.deleteVanDetails("{\"vanID\":71}"))); + } + + @Test + @DisplayName("updateZoneData should answer the van whose details it changed") + void updateVanDetails_shouldAnswerChangedVan() throws Exception { + M_Van stored = van(); + when(vanMasterServiceImpl.getVanByID(VAN_ID)).thenReturn(stored); + when(vanMasterServiceImpl.updateVanData(stored)).thenReturn(stored); + + assertSuccessContaining(controller.updateZoneData( + "{\"vanID\":71,\"vanName\":\"Mobile unit 9\",\"vehicalNo\":\"KA-01-AB-9999\",\"vanTypeID\":2," + + "\"stateID\":29,\"parkingPlaceID\":31,\"modifiedBy\":\"admin\"," + + "\"videoConsultationDomain\":\"psmri\"}"), + "Mobile unit 9"); + assertEquals("KA-01-AB-9999", stored.getVehicalNo()); + assertEquals("psmri", stored.getVideoConsultationDomain()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("updateZoneData should report the failure when the van is unknown") + void updateVanDetails_shouldReportUnknownVan() throws Exception { + when(vanMasterServiceImpl.getVanByID(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.updateZoneData("{\"vanID\":-1}"))); + } + + @Test + @DisplayName("saveVanTypeDetails should answer the van types it added") + void saveVanTypeDetails_shouldAnswerAddedTypes() throws Exception { + when(vanMasterServiceImpl.saveVanTypeDetails(anyList())).thenReturn( + new ArrayList<>(List.of(new M_VanType(2, "Diagnostic van", "Carries lab kit", Boolean.FALSE)))); + + assertSuccessContaining( + controller.saveVanTypeDetails("{\"vanTypeMaster\":[{\"vanType\":\"Diagnostic van\"}]}"), + "Diagnostic van"); + } + + @Test + @DisplayName("saveVanTypeDetails should report the failure when the van type cannot be added") + void saveVanTypeDetails_shouldReportStorageFailure() throws Exception { + when(vanMasterServiceImpl.saveVanTypeDetails(anyList())).thenThrow(new RuntimeException("already on file")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf( + controller.saveVanTypeDetails("{\"vanTypeMaster\":[{\"vanType\":\"Diagnostic van\"}]}"))); + } + + @Test + @DisplayName("getVanTypes should answer the van types on file") + void getVanTypes_shouldAnswerTypesOnFile() { + when(vanMasterServiceImpl.getVanTypes()).thenReturn( + new ArrayList<>(List.of(new M_VanType(2, "Diagnostic van", "Carries lab kit", Boolean.FALSE)))); + + assertSuccessContaining(controller.getVanTypes(), "Diagnostic van"); + } + + @Test + @DisplayName("getVanTypes should report the failure when the list cannot be answered") + void getVanTypes_shouldReportLookupFailure() { + when(vanMasterServiceImpl.getVanTypes()).thenThrow(new RuntimeException("connection reset")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getVanTypes())); + } + + @Test + @DisplayName("deleteVanType should confirm the retirement it recorded") + void deleteVanType_shouldConfirmRetirement() throws Exception { + when(vanMasterServiceImpl.updateVanTypeStatus(any(M_VanType.class))).thenReturn(1); + + assertSuccessContaining(controller.deleteVanType("{\"vanTypeID\":2,\"deleted\":true}"), + "status updated successfully"); + } + + @Test + @DisplayName("deleteVanType should say so when no van type was retired") + void deleteVanType_shouldSaySoWhenNothingRetired() throws Exception { + when(vanMasterServiceImpl.updateVanTypeStatus(any(M_VanType.class))).thenReturn(0); + + assertSuccessContaining(controller.deleteVanType("{\"vanTypeID\":-1,\"deleted\":true}"), + "Failed to update the status"); + } + + @Test + @DisplayName("deleteVanType should report the failure when the retirement cannot be recorded") + void deleteVanType_shouldReportRetirementFailure() throws Exception { + when(vanMasterServiceImpl.updateVanTypeStatus(any(M_VanType.class))) + .thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.deleteVanType("{\"vanTypeID\":2}"))); + } + + @Test + @DisplayName("getVanMaster should answer the vans of the provider and parking place asked for") + void getVanMaster_shouldAnswerVansOfProviderAndParkingPlace() throws Exception { + when(vanMasterServiceImpl.getVanMaster(PSM_ID, PARKING_PLACE_ID)).thenReturn(List.of(van())); + + assertSuccessContaining( + controller.getVanMaster("{\"providerServiceMapID\":4001,\"parkingPlaceID\":31}"), "Mobile unit 7"); + } + + @Test + @DisplayName("getVanMaster should report the failure when the lookup cannot be answered") + void getVanMaster_shouldReportLookupFailure() throws Exception { + when(vanMasterServiceImpl.getVanMaster(any(), any())).thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getVanMaster("{\"providerServiceMapID\":4001}"))); + } + + @Test + @DisplayName("getVanFromFacilityID should answer the vans belonging to the store asked for") + void getVanFromFacilityID_shouldAnswerVansOfStore() throws Exception { + M_Van request = new M_Van(); + request.setFacilityID(9001); + when(vanMasterServiceImpl.getVanFromFacilityID(9001)).thenReturn(List.of(van())); + + assertSuccessContaining(controller.getVanFromFacilityID(request), "Mobile unit 7"); + } + + @Test + @DisplayName("getVanFromFacilityID should report the failure when the store has no parking place") + void getVanFromFacilityID_shouldReportStoreWithoutParkingPlace() throws Exception { + M_Van request = new M_Van(); + request.setFacilityID(9001); + when(vanMasterServiceImpl.getVanFromFacilityID(9001)) + .thenThrow(new Exception("Main Store doesnt have any Parking place mapped")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getVanFromFacilityID(request))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/vanServicePointMapping/VanServicePointMappingControllerTest.java b/src/test/java/com/iemr/admin/controller/vanServicePointMapping/VanServicePointMappingControllerTest.java new file mode 100644 index 0000000..1aa87fd --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/vanServicePointMapping/VanServicePointMappingControllerTest.java @@ -0,0 +1,206 @@ +/* +* 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.admin.controller.vanServicePointMapping; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.vanServicePointMapping.M_VanServicePointMap; +import com.iemr.admin.service.vanServicePointMapping.VanServicePointMappingServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyList; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The van service point screen records which service points each van visits, + * adding new visits and changing the session of ones already on file. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VanServicePointMappingController Test Suite") +class VanServicePointMappingControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer VAN_ID = 71; + private static final Integer MAP_ID = 8801; + + @Mock + private VanServicePointMappingServiceImpl vanServicePointMappingServiceImpl; + + @InjectMocks + private VanServicePointMappingController controller; + + private static M_VanServicePointMap visit() { + return new M_VanServicePointMap(MAP_ID, VAN_ID, (short) 1, 88, "Attibele PHC", PSM_ID, Boolean.FALSE); + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + @Test + @DisplayName("saveVanServicePointMappings should store a brand new visit as it stands") + void save_shouldStoreBrandNewVisit() throws Exception { + when(vanServicePointMappingServiceImpl.saveVanServicePointMappings(anyList())) + .thenReturn(new ArrayList<>(List.of(visit()))); + + assertSuccessContaining(controller.saveVanServicePointMappings( + "{\"vanServicePointMappings\":[{\"vanID\":71,\"servicePointID\":88,\"vanSession\":1," + + "\"createdBy\":\"admin\"}]}"), + "Attibele PHC"); + verify(vanServicePointMappingServiceImpl, never()).getVanServicePointMappingByID(anyInt()); + } + + @Test + @DisplayName("saveVanServicePointMappings should change the session of a visit already on file") + void save_shouldChangeSessionOfExistingVisit() throws Exception { + M_VanServicePointMap stored = visit(); + when(vanServicePointMappingServiceImpl.getVanServicePointMappingByID(MAP_ID)).thenReturn(stored); + when(vanServicePointMappingServiceImpl.saveVanServicePointMappings(anyList())) + .thenReturn(new ArrayList<>(List.of(stored))); + + controller.saveVanServicePointMappings( + "{\"vanServicePointMappings\":[{\"vanServicePointMapID\":8801,\"vanSession\":2," + + "\"createdBy\":\"supervisor\"}]}"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(vanServicePointMappingServiceImpl).saveVanServicePointMappings(captor.capture()); + assertEquals((short) 2, captor.getValue().get(0).getVanSession()); + assertEquals("supervisor", captor.getValue().get(0).getModifiedBy()); + } + + @Test + @DisplayName("saveVanServicePointMappings should report the failure when the visit named is unknown") + void save_shouldReportUnknownVisit() throws Exception { + when(vanServicePointMappingServiceImpl.getVanServicePointMappingByID(anyInt())).thenReturn(null); + + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(controller.saveVanServicePointMappings( + "{\"vanServicePointMappings\":[{\"vanServicePointMapID\":8801,\"vanSession\":2}]}"))); + } + + @Test + @DisplayName("saveVanServicePointMappings should report the failure when the visits cannot be stored") + void save_shouldReportStorageFailure() throws Exception { + when(vanServicePointMappingServiceImpl.saveVanServicePointMappings(anyList())) + .thenThrow(new RuntimeException("service point already visited in that session")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.saveVanServicePointMappings( + "{\"vanServicePointMappings\":[{\"vanID\":71,\"servicePointID\":88}]}"))); + } + + @Test + @DisplayName("getVanServicePointMappings should answer the visits of the van asked about") + void get_shouldAnswerVisitsOfVan() throws Exception { + when(vanServicePointMappingServiceImpl.getAvailableVanServicePointMappings(31, VAN_ID, PSM_ID)) + .thenReturn(new ArrayList<>(List.of(visit()))); + + assertSuccessContaining(controller.getVanServicePointMappings( + "{\"parkingPlaceID\":31,\"vanID\":71,\"providerServiceMapID\":4001}"), "Attibele PHC"); + } + + @Test + @DisplayName("getVanServicePointMappings should report the failure when the visits cannot be answered") + void get_shouldReportLookupFailure() throws Exception { + when(vanServicePointMappingServiceImpl.getAvailableVanServicePointMappings(any(), any(), any())) + .thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getVanServicePointMappings("{\"vanID\":71}"))); + } + + @Test + @DisplayName("vanServicePointMappingsV1 should answer the visits with their district and taluk") + void getV1_shouldAnswerVisitsWithLocation() throws Exception { + when(vanServicePointMappingServiceImpl.getAvailableVanServicePointMappingsV1(31, VAN_ID, PSM_ID)) + .thenReturn(new ArrayList<>(List.of(new M_VanServicePointMap(MAP_ID, VAN_ID, (short) 1, 88, + "Attibele PHC", PSM_ID, Boolean.FALSE, 301, "Bengaluru Urban", 3011, "Anekal")))); + + assertSuccessContaining(controller.vanServicePointMappingsV1( + "{\"parkingPlaceID\":31,\"vanID\":71,\"providerServiceMapID\":4001}"), "Anekal"); + } + + @Test + @DisplayName("vanServicePointMappingsV1 should report the failure when the visits cannot be answered") + void getV1_shouldReportLookupFailure() throws Exception { + when(vanServicePointMappingServiceImpl.getAvailableVanServicePointMappingsV1(any(), any(), any())) + .thenThrow(new RuntimeException("connection reset")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.vanServicePointMappingsV1("{\"vanID\":71}"))); + } + + @Test + @DisplayName("deleteVanServicePointMappingDetails should confirm the retirement it recorded") + void delete_shouldConfirmRetirement() throws Exception { + when(vanServicePointMappingServiceImpl.updateVanServicePointMappingStatus(any(M_VanServicePointMap.class))) + .thenReturn(1); + + assertSuccessContaining(controller.deleteVanServicePointMappingDetails( + "{\"vanServicePointMapID\":8801,\"deleted\":true,\"modifiedBy\":\"admin\"}"), + "status updated successfully"); + } + + @Test + @DisplayName("deleteVanServicePointMappingDetails should say so when no visit was retired") + void delete_shouldSaySoWhenNothingRetired() throws Exception { + when(vanServicePointMappingServiceImpl.updateVanServicePointMappingStatus(any(M_VanServicePointMap.class))) + .thenReturn(0); + + assertSuccessContaining( + controller.deleteVanServicePointMappingDetails("{\"vanServicePointMapID\":-1,\"deleted\":true}"), + "Failed to update the status"); + } + + @Test + @DisplayName("deleteVanServicePointMappingDetails should report the failure when the retirement cannot be recorded") + void delete_shouldReportStorageFailure() throws Exception { + when(vanServicePointMappingServiceImpl.updateVanServicePointMappingStatus(any(M_VanServicePointMap.class))) + .thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf( + controller.deleteVanServicePointMappingDetails("{\"vanServicePointMapID\":8801}"))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/vanSpokeMapping/VanSpokeMappingControllerTest.java b/src/test/java/com/iemr/admin/controller/vanSpokeMapping/VanSpokeMappingControllerTest.java new file mode 100644 index 0000000..590fa56 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/vanSpokeMapping/VanSpokeMappingControllerTest.java @@ -0,0 +1,159 @@ +/* +* 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.admin.controller.vanSpokeMapping; + +import org.json.JSONObject; +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.admin.service.vanSpokeMapping.VanSpokeMappingService; +import com.iemr.admin.utils.exception.IEMRException; +import com.iemr.admin.utils.response.OutputResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * The spoke mapping screen ties mobile unit vans to the telemedicine spokes + * they serve. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VanSpokeMappingController Test Suite") +class VanSpokeMappingControllerTest { + + private static final String AUTHORIZATION = "session-key-123"; + private static final String REQUEST = "{\"vanSpokeMapping\":[{\"mmu_VanID\":71}]}"; + + @Mock + private VanSpokeMappingService vanSpokeMappingService; + + @InjectMocks + private VanSpokeMappingController controller; + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static String errorMessageOf(String response) { + return new JSONObject(response).getString("errorMessage"); + } + + @Test + @DisplayName("saving a mapping should confirm the tie it recorded") + void save_shouldConfirmRecordedTie() throws Exception { + when(vanSpokeMappingService.saveVanSpokeMapping(anyString())).thenReturn("success"); + + String response = controller.saveBenNCDCareNurseData(REQUEST, AUTHORIZATION); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Mapping done successfully"), response); + } + + @Test + @DisplayName("saving a mapping should report the failure when the tie was refused") + void save_shouldReportRefusedTie() throws Exception { + when(vanSpokeMappingService.saveVanSpokeMapping(anyString())).thenReturn("failure"); + + assertEquals("error in mapping van and spoke", + errorMessageOf(controller.saveBenNCDCareNurseData(REQUEST, AUTHORIZATION))); + } + + @Test + @DisplayName("saving a mapping should report the failure when the tie cannot be recorded") + void save_shouldReportStorageFailure() throws Exception { + when(vanSpokeMappingService.saveVanSpokeMapping(anyString())) + .thenThrow(new IEMRException("van is already mapped")); + + assertEquals(OutputResponse.USERID_FAILURE, + statusCodeOf(controller.saveBenNCDCareNurseData(REQUEST, AUTHORIZATION))); + } + + @Test + @DisplayName("getVanSpokeMapping should answer the ties held at the parking place asked about") + void get_shouldAnswerHeldTies() throws Exception { + when(vanSpokeMappingService.getVanSpokeMappingDetails(anyString())) + .thenReturn("{\"vanSpokeMappedDetails\":[{\"vanspokeID\":6001}]}"); + + String response = controller.getVanSpokeMapping(REQUEST, AUTHORIZATION); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("vanSpokeMappedDetails"), response); + } + + @Test + @DisplayName("getVanSpokeMapping should report the failure when nothing came back from the service") + void get_shouldReportMissingAnswer() throws Exception { + when(vanSpokeMappingService.getVanSpokeMappingDetails(anyString())).thenReturn(null); + + assertEquals("error in fetching the van and spoke data", + errorMessageOf(controller.getVanSpokeMapping(REQUEST, AUTHORIZATION))); + } + + @Test + @DisplayName("getVanSpokeMapping should report the failure when the lookup cannot be answered") + void get_shouldReportLookupFailure() throws Exception { + when(vanSpokeMappingService.getVanSpokeMappingDetails(anyString())) + .thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.getVanSpokeMapping(REQUEST, AUTHORIZATION))); + } + + @Test + @DisplayName("deleteVanSpokeMapping should confirm the change it recorded") + void delete_shouldConfirmRecordedChange() throws Exception { + when(vanSpokeMappingService.deleteVanSpokeMapping(anyString())).thenReturn("success"); + + String response = controller.deleteVanSpokeMapping(REQUEST, AUTHORIZATION); + + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains("Mapping status got updated"), response); + } + + @Test + @DisplayName("deleteVanSpokeMapping should report the failure when the change was refused") + void delete_shouldReportRefusedChange() throws Exception { + when(vanSpokeMappingService.deleteVanSpokeMapping(anyString())).thenReturn("failure"); + + assertEquals("Error in deleting mapping", + errorMessageOf(controller.deleteVanSpokeMapping(REQUEST, AUTHORIZATION))); + } + + @Test + @DisplayName("deleteVanSpokeMapping should report the failure when the change cannot be recorded") + void delete_shouldReportStorageFailure() throws Exception { + when(vanSpokeMappingService.deleteVanSpokeMapping(anyString())) + .thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.deleteVanSpokeMapping(REQUEST, AUTHORIZATION))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/version/VersionControllerTest.java b/src/test/java/com/iemr/admin/controller/version/VersionControllerTest.java new file mode 100644 index 0000000..1363758 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/version/VersionControllerTest.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.admin.controller.version; + +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The version endpoint tells an operator which build is deployed, and must + * answer something rather than fail when the build stamp is missing. + */ +@DisplayName("VersionController Test Suite") +class VersionControllerTest { + + private final VersionController controller = new VersionController(); + + @Test + @DisplayName("versionInformation should answer the build stamp fields the deployment records") + void versionInformation_shouldAnswerBuildStampFields() { + ResponseEntity> response = controller.versionInformation(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.getBody().containsKey("buildTimestamp")); + assertTrue(response.getBody().containsKey("version")); + assertTrue(response.getBody().containsKey("branch")); + assertTrue(response.getBody().containsKey("commitHash")); + } + + @Test + @DisplayName("versionInformation should answer the same stamp on every call") + void versionInformation_shouldAnswerConsistently() { + assertEquals(controller.versionInformation().getBody(), controller.versionInformation().getBody()); + } +} diff --git a/src/test/java/com/iemr/admin/controller/villageMaster/VillageMasterControllerTest.java b/src/test/java/com/iemr/admin/controller/villageMaster/VillageMasterControllerTest.java new file mode 100644 index 0000000..f56006e --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/villageMaster/VillageMasterControllerTest.java @@ -0,0 +1,177 @@ +/* +* 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.admin.controller.villageMaster; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.locationmaster.DistrictBranchMapping; +import com.iemr.admin.service.villageMaster.VillageMasterServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +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.anyList; +import static org.mockito.Mockito.when; + +/** + * The village screen keeps the villages of a taluk and the details each one is + * addressed by. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VillageMasterController Test Suite") +class VillageMasterControllerTest { + + private static final Integer BLOCK_ID = 3011; + + @Mock + private VillageMasterServiceImpl villageMasterServiceImpl; + + @InjectMocks + private VillageMasterController controller; + + private static DistrictBranchMapping village() { + DistrictBranchMapping village = new DistrictBranchMapping(); + village.setDistrictBranchID(30111); + village.setVillageName("Attibele"); + village.setPinCode("562107"); + return village; + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String expected) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(expected), response); + } + + @Test + @DisplayName("saveVillageDetails should answer the villages it added") + void save_shouldAnswerAddedVillages() { + when(villageMasterServiceImpl.storeVillageDetails(anyList())) + .thenReturn(new ArrayList<>(List.of(village()))); + + assertSuccessContaining(controller.saveVillageDetails( + "{\"districtBranchMapping\":[{\"villageName\":\"Attibele\",\"blockID\":3011}]}"), "Attibele"); + } + + @Test + @DisplayName("saveVillageDetails should report the failure when the village cannot be added") + void save_shouldReportStorageFailure() { + when(villageMasterServiceImpl.storeVillageDetails(anyList())) + .thenThrow(new RuntimeException("village already on file")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.saveVillageDetails( + "{\"districtBranchMapping\":[{\"villageName\":\"Attibele\"}]}"))); + } + + @Test + @DisplayName("getVillages should answer the villages of the taluk asked about") + void get_shouldAnswerVillagesOfTaluk() { + when(villageMasterServiceImpl.getAvailableVillages(BLOCK_ID)) + .thenReturn(new ArrayList<>(List.of(village()))); + + assertSuccessContaining(controller.getVillages("{\"blockID\":3011}"), "Attibele"); + } + + @Test + @DisplayName("getVillages should report the failure when the villages cannot be answered") + void get_shouldReportLookupFailure() { + when(villageMasterServiceImpl.getAvailableVillages(any())) + .thenThrow(new RuntimeException("query timed out")); + + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(controller.getVillages("{\"blockID\":3011}"))); + } + + @Test + @DisplayName("deleteVillage should confirm the retirement it recorded") + void delete_shouldConfirmRetirement() { + when(villageMasterServiceImpl.updateVillageStatus(any(DistrictBranchMapping.class))).thenReturn(1); + + assertSuccessContaining( + controller.deleteVillage("{\"districtBranchID\":30111,\"deleted\":true,\"modifiedBy\":\"admin\"}"), + "status updated successfully"); + } + + @Test + @DisplayName("deleteVillage should say so when no village was retired") + void delete_shouldSaySoWhenNothingRetired() { + when(villageMasterServiceImpl.updateVillageStatus(any(DistrictBranchMapping.class))).thenReturn(0); + + assertSuccessContaining(controller.deleteVillage("{\"districtBranchID\":-1,\"deleted\":true}"), + "Failed to update the status"); + } + + @Test + @DisplayName("deleteVillage should report the failure when the retirement cannot be recorded") + void delete_shouldReportStorageFailure() { + when(villageMasterServiceImpl.updateVillageStatus(any(DistrictBranchMapping.class))) + .thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.deleteVillage("{\"districtBranchID\":30111}"))); + } + + @Test + @DisplayName("updateVillageData should confirm the change it recorded") + void update_shouldConfirmRecordedChange() { + when(villageMasterServiceImpl.updateVillageData(any(DistrictBranchMapping.class))).thenReturn(1); + + assertSuccessContaining(controller.updateVillageData( + "{\"districtBranchID\":30111,\"villageName\":\"Attibele\",\"pinCode\":\"562107\"," + + "\"modifiedBy\":\"admin\"}"), + "status updated successfully"); + } + + @Test + @DisplayName("updateVillageData should say so when no village was changed") + void update_shouldSaySoWhenNothingChanged() { + when(villageMasterServiceImpl.updateVillageData(any(DistrictBranchMapping.class))).thenReturn(0); + + assertSuccessContaining(controller.updateVillageData("{\"districtBranchID\":-1}"), + "Failed to update the status"); + } + + @Test + @DisplayName("updateVillageData should report the failure when the change cannot be recorded") + void update_shouldReportStorageFailure() { + when(villageMasterServiceImpl.updateVillageData(any(DistrictBranchMapping.class))) + .thenThrow(new RuntimeException("row is locked")); + + assertEquals(OutputResponse.GENERIC_FAILURE, + statusCodeOf(controller.updateVillageData("{\"districtBranchID\":30111}"))); + } +} diff --git a/src/test/java/com/iemr/admin/controller/zonemaster/ZoneMasterControllerTest.java b/src/test/java/com/iemr/admin/controller/zonemaster/ZoneMasterControllerTest.java new file mode 100644 index 0000000..ed87be6 --- /dev/null +++ b/src/test/java/com/iemr/admin/controller/zonemaster/ZoneMasterControllerTest.java @@ -0,0 +1,287 @@ +/* +* 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.admin.controller.zonemaster; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONObject; +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.admin.data.zonemaster.M_Zone; +import com.iemr.admin.data.zonemaster.M_ZoneDistrictMap; +import com.iemr.admin.service.zonemaster.ZoneMasterServiceImpl; +import com.iemr.admin.utils.response.OutputResponse; + +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The zone endpoints group the districts a provider operates in, and a zone that + * is retired has to take its district mappings with it. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ZoneMasterController Test Suite") +class ZoneMasterControllerTest { + + private static final Integer PSM_ID = 4001; + private static final Integer ZONE_ID = 61; + + @Mock + private ZoneMasterServiceImpl zoneMasterServiceImpl; + + @InjectMocks + private ZoneMasterController controller; + + private static M_Zone zone(Integer id, String name) { + return new M_Zone(id, name, "Northern districts", "Main Road", PSM_ID, Boolean.FALSE, 1, "India", + 29, "Karnataka", 301, "Bengaluru Urban", 401, "North block", 501, "Hosur", null, 3, + "Tele Medicine"); + } + + private static M_ZoneDistrictMap districtMapping(Integer id) { + return new M_ZoneDistrictMap(id, ZONE_ID, "North zone", 301, PSM_ID, Boolean.FALSE, 29, "Karnataka", + "Bengaluru Urban", 3, "Tele Medicine", Boolean.FALSE); + } + + private static int statusCodeOf(String response) { + return new JSONObject(response).getInt("statusCode"); + } + + private static void assertSuccessContaining(String response, String fragment) { + assertEquals(OutputResponse.SUCCESS, statusCodeOf(response), response); + assertTrue(response.contains(fragment), response); + } + + private static void assertGenericFailure(String response) { + assertEquals(OutputResponse.GENERIC_FAILURE, statusCodeOf(response), response); + } + + private static void assertCodeException(String response) { + assertEquals(OutputResponse.CODE_EXCEPTION, statusCodeOf(response), response); + } + + @Test + @DisplayName("saveZone should answer the zones the service stored") + void saveZone_shouldAnswerStoredZones() throws Exception { + when(zoneMasterServiceImpl.createZone(anyList())) + .thenReturn(new ArrayList<>(List.of(zone(ZONE_ID, "North zone")))); + + assertSuccessContaining(controller.saveZone("{\"zones\":[{\"zoneName\":\"North zone\"}]}"), "North zone"); + } + + @Test + @DisplayName("saveZone should answer an error envelope when the store fails") + void saveZone_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(zoneMasterServiceImpl.createZone(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.saveZone("{\"zones\":[{\"zoneName\":\"North zone\"}]}")); + } + + @Test + @DisplayName("getZones should answer the zones of the provider") + void getZones_shouldAnswerProviderZones() { + when(zoneMasterServiceImpl.getAvailableZones(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(zone(ZONE_ID, "North zone")))); + + assertSuccessContaining(controller.getZones("{\"providerServiceMapID\":4001}"), "North zone"); + } + + @Test + @DisplayName("getZones should ask the caller for a provider when the request names none") + void getZones_shouldAskForProvider() { + assertSuccessContaining(controller.getZones("{}"), "Provide providerServiceMapID."); + verify(zoneMasterServiceImpl, never()).getAvailableZones(anyInt()); + } + + @Test + @DisplayName("getZones should stay at its default for a provider id of zero") + void getZones_shouldStayAtDefaultForZeroProvider() { + assertGenericFailure(controller.getZones("{\"providerServiceMapID\":0}")); + } + + @Test + @DisplayName("getZones should answer an error envelope for a body it cannot read") + void getZones_shouldAnswerErrorEnvelopeForMalformedBody() { + assertEquals(OutputResponse.OBJECT_FAILURE, statusCodeOf(controller.getZones("not json"))); + } + + @Test + @DisplayName("mapZoneWithDistrict should answer the mappings the service stored") + void mapZoneWithDistrict_shouldAnswerStoredMappings() throws Exception { + when(zoneMasterServiceImpl.createZoneDistrictMapping(anyList())) + .thenReturn(new ArrayList<>(List.of(districtMapping(9001)))); + + assertSuccessContaining(controller.mapZoneWithDistrict( + "{\"zoneDistrictMappings\":[{\"zoneID\":61,\"districtID\":301}]}"), "9001"); + } + + @Test + @DisplayName("mapZoneWithDistrict should answer an error envelope when the store fails") + void mapZoneWithDistrict_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(zoneMasterServiceImpl.createZoneDistrictMapping(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.mapZoneWithDistrict("{\"zoneDistrictMappings\":[{}]}")); + } + + @Test + @DisplayName("editZoneDistrict should copy the edits onto the stored mapping") + void editZoneDistrict_shouldCopyEdits() throws Exception { + M_ZoneDistrictMap stored = districtMapping(9001); + when(zoneMasterServiceImpl.editZoneDistrictMapping(9001)).thenReturn(stored); + when(zoneMasterServiceImpl.saveeditedData(stored)).thenReturn(stored); + + String response = controller.editZoneDistrict("{\"zoneDistrictMapID\":9001,\"zoneID\":62," + + "\"districtID\":302,\"providerServiceMapID\":4001,\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "9001"); + assertEquals(62, stored.getZoneID()); + assertEquals(302, stored.getDistrictID()); + } + + @Test + @DisplayName("editZoneDistrict should answer an error envelope for a mapping that does not exist") + void editZoneDistrict_shouldAnswerErrorEnvelopeForUnknownMapping() throws Exception { + when(zoneMasterServiceImpl.editZoneDistrictMapping(9001)).thenReturn(null); + + assertCodeException(controller.editZoneDistrict("{\"zoneDistrictMapID\":9001}")); + } + + @Test + @DisplayName("getZoneDistrictMappings should answer the district mappings of the provider") + void getZoneDistrictMappings_shouldAnswerProviderMappings() { + when(zoneMasterServiceImpl.getAvailableZoneDistrictMappings(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(districtMapping(9001)))); + + assertSuccessContaining( + controller.getZoneDistrictMappings("{\"providerServiceMapID\":4001}"), "9001"); + } + + @Test + @DisplayName("getZoneDistrictMappings should ask the caller for a provider when the request names none") + void getZoneDistrictMappings_shouldAskForProvider() { + assertSuccessContaining(controller.getZoneDistrictMappings("{}"), "Provide serviceProviderID."); + } + + @Test + @DisplayName("deleteZone should report whether the zone was actually retired") + void deleteZone_shouldReportOutcome() throws Exception { + when(zoneMasterServiceImpl.updateZoneStatus(any())).thenReturn(1); + assertSuccessContaining(controller.deleteZone("{\"zoneID\":61,\"deleted\":true}"), + "status updated successfully"); + + when(zoneMasterServiceImpl.updateZoneStatus(any())).thenReturn(0); + assertSuccessContaining(controller.deleteZone("{\"zoneID\":61,\"deleted\":true}"), + "Failed to update the status"); + } + + @Test + @DisplayName("deleteZone should answer an error envelope when the change fails") + void deleteZone_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(zoneMasterServiceImpl.updateZoneStatus(any())).thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.deleteZone("{\"zoneID\":61,\"deleted\":true}")); + } + + @Test + @DisplayName("updateZoneData should copy the edits onto the stored zone") + void updateZoneData_shouldCopyEdits() throws Exception { + M_Zone stored = zone(ZONE_ID, "old name"); + when(zoneMasterServiceImpl.getzoneByID(ZONE_ID)).thenReturn(stored); + when(zoneMasterServiceImpl.updateZoneData(stored)).thenReturn(stored); + + String response = controller.updateZoneData("{\"zoneID\":61,\"zoneName\":\"North zone\"," + + "\"zoneDesc\":\"Northern districts\",\"zoneHQAddress\":\"Main Road\",\"stateID\":29," + + "\"districtID\":301,\"districtBlockID\":401,\"districtBranchID\":501," + + "\"modifiedBy\":\"admin\"}"); + + assertSuccessContaining(response, "North zone"); + assertEquals("Northern districts", stored.getZoneDesc()); + assertEquals(29, stored.getStateID()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("updateZoneData should answer an error envelope for a zone that does not exist") + void updateZoneData_shouldAnswerErrorEnvelopeForUnknownZone() throws Exception { + when(zoneMasterServiceImpl.getzoneByID(ZONE_ID)).thenReturn(null); + + assertCodeException(controller.updateZoneData("{\"zoneID\":61}")); + } + + @Test + @DisplayName("getMappedDistrictByZoneID should answer the districts mapped to the zone") + void getMappedDistrictByZoneID_shouldAnswerMappedDistricts() { + when(zoneMasterServiceImpl.editZoneDistrictMapping1(ZONE_ID)) + .thenReturn(new ArrayList<>(List.of(districtMapping(9001)))); + + assertSuccessContaining(controller.getMappedDistrictByZoneID("{\"zoneID\":61}"), "9001"); + } + + @Test + @DisplayName("getMappedDistrictByZoneID should answer an error envelope when the lookup fails") + void getMappedDistrictByZoneID_shouldAnswerErrorEnvelopeOnFailure() { + when(zoneMasterServiceImpl.editZoneDistrictMapping1(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure(controller.getMappedDistrictByZoneID("{\"zoneID\":61}")); + } + + @Test + @DisplayName("deleteZoneDistrictMapping should report whether the mapping changed") + void deleteZoneDistrictMapping_shouldReportOutcome() throws Exception { + when(zoneMasterServiceImpl.updateZoneDistrictMappingStatus(any())).thenReturn(0); + assertSuccessContaining( + controller.deleteZoneDistrictMapping("{\"zoneDistrictMapID\":9001,\"deleted\":true}"), + "status updated successfully"); + + when(zoneMasterServiceImpl.updateZoneDistrictMappingStatus(any())).thenReturn(1); + assertSuccessContaining( + controller.deleteZoneDistrictMapping("{\"zoneDistrictMapID\":9001,\"deleted\":true}"), + "Failed to update the status"); + } + + @Test + @DisplayName("deleteZoneDistrictMapping should answer an error envelope when the change fails") + void deleteZoneDistrictMapping_shouldAnswerErrorEnvelopeOnFailure() throws Exception { + when(zoneMasterServiceImpl.updateZoneDistrictMappingStatus(any())) + .thenThrow(new IllegalStateException("no connection")); + + assertGenericFailure( + controller.deleteZoneDistrictMapping("{\"zoneDistrictMapID\":9001,\"deleted\":true}")); + } +} diff --git a/src/test/java/com/iemr/admin/data/BeanContract.java b/src/test/java/com/iemr/admin/data/BeanContract.java new file mode 100644 index 0000000..379b493 --- /dev/null +++ b/src/test/java/com/iemr/admin/data/BeanContract.java @@ -0,0 +1,245 @@ +/* +* 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.admin.data; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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; + +/** + * Reflective checks for the accessors, equality and string representation of the + * plain data carriers this service exchanges over its APIs. + * + *

The carriers are mostly Lombok {@code @Data} entities, so the generated + * {@code equals}, {@code hashCode} and {@code toString} are exercised alongside + * every readable/writable property. A carrier that cannot be built through a + * no-argument constructor is reported by {@link #isVerifiable(Class)} so the + * calling suite can skip it rather than fail. + */ +public final class BeanContract { + + private BeanContract() { + } + + /** Answers whether the type can be exercised through the reflective contract. */ + public static boolean isVerifiable(Class type) { + if (type.isInterface() || type.isEnum() || type.isAnnotation() + || Modifier.isAbstract(type.getModifiers()) + || type.isMemberClass() && !Modifier.isStatic(type.getModifiers())) { + return false; + } + try { + type.getDeclaredConstructor(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + public static void verify(Class type) throws Exception { + Object left = newInstance(type); + Object right = newInstance(type); + copyFields(type, left, right); + + assertNotNull(left.toString(), type.getSimpleName() + " must render a string form"); + assertTrue(left.equals(left), type.getSimpleName() + " must equal itself"); + assertFalse(left.equals(null), type.getSimpleName() + " must never equal null"); + assertFalse(left.equals(new Object()), type.getSimpleName() + " must never equal an unrelated type"); + left.hashCode(); + + if (left.equals(right)) { + assertEquals(left.hashCode(), right.hashCode(), + type.getSimpleName() + " must hash consistently with equals"); + } + + verifyProperties(type, left, right); + } + + private static Object newInstance(Class type) throws Exception { + java.lang.reflect.Constructor constructor = type.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } + + private static void verifyProperties(Class type, Object left, Object right) throws Exception { + for (Field field : type.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers())) { + continue; + } + Method getter = findGetter(type, field); + Method setter = findSetter(type, field); + if (getter == null || setter == null) { + continue; + } + Object value = sampleValue(field.getType()); + if (value == null) { + continue; + } + + setter.invoke(left, value); + assertEquals(value, getter.invoke(left), + type.getSimpleName() + "." + field.getName() + " must round-trip through its accessors"); + + setter.invoke(right, value); + assertEquals(getter.invoke(left), getter.invoke(right), + type.getSimpleName() + "." + field.getName() + " must read back the same on both instances"); + } + assertNotNull(left.toString(), type.getSimpleName() + " must render a populated string form"); + left.hashCode(); + left.equals(right); + } + + private static void copyFields(Class type, Object from, Object to) throws Exception { + for (Field field : type.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers())) { + continue; + } + field.setAccessible(true); + field.set(to, field.get(from)); + } + } + + private static Method findGetter(Class type, Field field) { + String suffix = capitalise(field.getName()); + for (String prefix : new String[] { "get", "is" }) { + try { + Method candidate = type.getMethod(prefix + suffix); + if (candidate.getParameterCount() == 0) { + return candidate; + } + } catch (NoSuchMethodException ignored) { + // try the next accessor style + } + } + return null; + } + + private static Method findSetter(Class type, Field field) { + try { + return type.getMethod("set" + capitalise(field.getName()), field.getType()); + } catch (NoSuchMethodException e) { + return null; + } + } + + private static String capitalise(String name) { + return Character.toUpperCase(name.charAt(0)) + name.substring(1); + } + + private static Object sampleValue(Class type) { + if (type == String.class) { + return "sample"; + } + if (type == Long.class || type == long.class) { + return 7L; + } + if (type == Integer.class || type == int.class) { + return 7; + } + if (type == Short.class || type == short.class) { + return (short) 7; + } + if (type == Double.class || type == double.class) { + return 7.5d; + } + if (type == Float.class || type == float.class) { + return 7.5f; + } + if (type == Character.class || type == char.class) { + return 'y'; + } + if (type == Byte.class || type == byte.class) { + return (byte) 7; + } + if (type == Boolean.class || type == boolean.class) { + return Boolean.TRUE; + } + if (type == java.math.BigDecimal.class) { + return java.math.BigDecimal.valueOf(7.5d); + } + if (type == BigInteger.class) { + return BigInteger.valueOf(7L); + } + if (type == Timestamp.class) { + return Timestamp.valueOf("2026-02-17 09:30:00"); + } + if (type == Date.class) { + return Date.valueOf("2026-02-17"); + } + if (type == Time.class) { + return Time.valueOf("09:30:00"); + } + if (type == java.util.Date.class) { + return new java.util.Date(1_771_286_400_000L); + } + if (type == LocalDate.class) { + return LocalDate.of(2026, 2, 17); + } + if (type == LocalTime.class) { + return LocalTime.of(9, 30); + } + if (type == LocalDateTime.class) { + return LocalDateTime.of(2026, 2, 17, 9, 30); + } + if (type == List.class) { + return new ArrayList<>(List.of("first", "second")); + } + if (type == Set.class) { + return new HashSet<>(Set.of("first")); + } + if (type == Map.class) { + return new HashMap<>(Map.of("key", "value")); + } + if (type == Object.class) { + return "sample"; + } + if (type.isPrimitive() || type.isEnum() || type.isArray() || type.isInterface()) { + return null; + } + try { + java.lang.reflect.Constructor constructor = type.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (ReflectiveOperationException | SecurityException e) { + // A carrier that needs arguments is simply left at its default value. + return null; + } + } +} diff --git a/src/test/java/com/iemr/admin/data/ClassScanner.java b/src/test/java/com/iemr/admin/data/ClassScanner.java new file mode 100644 index 0000000..7eb63b0 --- /dev/null +++ b/src/test/java/com/iemr/admin/data/ClassScanner.java @@ -0,0 +1,64 @@ +/* +* 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.admin.data; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.RegexPatternTypeFilter; + +/** Finds every concrete class published under a package, for suite-wide contract checks. */ +public final class ClassScanner { + + private ClassScanner() { + } + + public static List> classesUnder(String... packages) { + ClassPathScanningCandidateComponentProvider provider = + new ClassPathScanningCandidateComponentProvider(false) { + @Override + protected boolean isCandidateComponent( + org.springframework.beans.factory.annotation.AnnotatedBeanDefinition definition) { + return definition.getMetadata().isIndependent() + && !definition.getMetadata().isAnnotation(); + } + }; + provider.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(".*"))); + + List> found = new ArrayList<>(); + for (String packageName : packages) { + for (BeanDefinition definition : provider.findCandidateComponents(packageName)) { + try { + found.add(Class.forName(definition.getBeanClassName())); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // A class the test classpath cannot resolve is not part of the contract. + } + } + } + found.sort(Comparator.comparing(Class::getName)); + return found; + } +} diff --git a/src/test/java/com/iemr/admin/data/DataCarrierContractTest.java b/src/test/java/com/iemr/admin/data/DataCarrierContractTest.java new file mode 100644 index 0000000..1fe9016 --- /dev/null +++ b/src/test/java/com/iemr/admin/data/DataCarrierContractTest.java @@ -0,0 +1,71 @@ +/* +* 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.admin.data; + +import java.util.List; + +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; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Exercises every data carrier the service exchanges - the JPA entities, the + * transfer objects and the small model holders - against the accessor, equality + * and string contract their callers rely on. + */ +@DisplayName("Data carrier contract Test Suite") +class DataCarrierContractTest { + + private static final String[] CARRIER_PACKAGES = { + "com.iemr.admin.data", + "com.iemr.admin.to", + "com.iemr.admin.model" }; + + static List> carriers() { + return ClassScanner.classesUnder(CARRIER_PACKAGES).stream() + .filter(type -> !type.getName().endsWith("Test")) + .filter(type -> !type.getSimpleName().equals("BeanContract")) + .filter(type -> !type.getSimpleName().equals("ClassScanner")) + .filter(BeanContract::isVerifiable) + .toList(); + } + + @Test + @DisplayName("the scan should discover the carriers rather than silently pass on an empty set") + void carrierScan_shouldDiscoverCarriers() { + List> carriers = carriers(); + assertFalse(carriers.isEmpty(), "no data carriers were discovered on the test classpath"); + assertTrue(carriers.size() > 100, + "expected the full carrier set, found only " + carriers.size()); + } + + @ParameterizedTest(name = "{0} honours the accessor, equality and string contract") + @DisplayName("every data carrier should honour its accessor, equality and string contract") + @MethodSource("carriers") + void dataCarrier_shouldHonourBeanContract(Class type) throws Exception { + BeanContract.verify(type); + } +} diff --git a/src/test/java/com/iemr/admin/mapper/emailconfig/InstituteEmailConfigMapperTest.java b/src/test/java/com/iemr/admin/mapper/emailconfig/InstituteEmailConfigMapperTest.java new file mode 100644 index 0000000..a80f1de --- /dev/null +++ b/src/test/java/com/iemr/admin/mapper/emailconfig/InstituteEmailConfigMapperTest.java @@ -0,0 +1,260 @@ +/* +* 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.admin.mapper.emailconfig; + +import java.sql.Timestamp; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.admin.data.emailconfig.AuthorityEmail; +import com.iemr.admin.model.emailconfig.AuthEmailRequest; +import com.iemr.admin.model.emailconfig.AuthEmailResponse; +import com.iemr.admin.model.emailconfig.CreateAuthEmailRequestModel; +import com.iemr.admin.model.emailconfig.CreateNodalEmailRequestModel; +import com.iemr.admin.model.emailconfig.NodalEmailResponse; +import com.iemr.admin.model.emailconfig.UpdateAuthEmailRequest; +import com.iemr.admin.model.emailconfig.UpdateNodalEmailRequest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The escalation emails a nodal officer receives are addressed from these + * records, so the mapper must carry every contact field across rather than drop + * one silently. + */ +@DisplayName("InstituteEmailConfigMapper Test Suite") +class InstituteEmailConfigMapperTest { + + private final InstituteEmailConfigMapper mapper = InstituteEmailConfigMapper.INSTANCE; + + private static AuthorityEmail storedRecord() { + AuthorityEmail stored = new AuthorityEmail(); + stored.setAuthorityEmailID(9001); + stored.setStateID(29); + stored.setDistrictID(301); + stored.setBlockID(401); + stored.setDistrictBranchMappingID(501); + stored.setDesignationID(7); + stored.setAuthorityName("Dr Asha Rao"); + stored.setEmailID("asha.rao@example.org"); + stored.setContactNo("9000000001"); + stored.setProviderServiceMapID(4001); + stored.setCreatedBy("admin"); + stored.setCreatedDate(Timestamp.valueOf("2026-02-17 09:30:00")); + stored.setModifiedBy("admin"); + stored.setDeleted(Boolean.FALSE); + return stored; + } + + private static CreateAuthEmailRequestModel createAuthorityRequest() { + CreateAuthEmailRequestModel request = new CreateAuthEmailRequestModel(); + request.setStateID(29); + request.setDistrictID(301); + request.setBlockID(401); + request.setDistrictBranchMappingID(501); + request.setDesignationID(7); + request.setAuthorityName("Dr Asha Rao"); + request.setEmailID("asha.rao@example.org"); + request.setContactNo("9000000001"); + request.setProviderServiceMapID(4001); + request.setCreatedBy("admin"); + return request; + } + + private static CreateNodalEmailRequestModel createNodalRequest() { + CreateNodalEmailRequestModel request = new CreateNodalEmailRequestModel(); + request.setStateID(29); + request.setDistrictID(301); + request.setDesignationID(7); + request.setAuthorityName("Dr Asha Rao"); + request.setEmailID("asha.rao@example.org"); + request.setContactNo("9000000001"); + request.setMobileNo("9000000002"); + request.setProviderServiceMapID(4001); + request.setCreatedBy("admin"); + return request; + } + + @Test + @DisplayName("requestToInstituteEmailConf should carry the search request onto the record") + void requestToInstituteEmailConf_shouldCarrySearchRequest() { + AuthEmailRequest request = new AuthEmailRequest(); + request.setAuthorityEmailID(9001); + request.setStateID(29); + request.setDistrictID(301); + request.setBlockID(401); + request.setDistrictBranchMappingID(501); + request.setDesignationID(7); + request.setProviderServiceMapID(4001); + request.setDeleted(Boolean.FALSE); + + AuthorityEmail mapped = mapper.requestToInstituteEmailConf(request); + + assertEquals(9001, mapped.getAuthorityEmailID()); + assertEquals(29, mapped.getStateID()); + assertEquals(501, mapped.getDistrictBranchMappingID()); + assertEquals(4001, mapped.getProviderServiceMapID()); + } + + @Test + @DisplayName("requestToInstituteEmailConf should map a whole batch of search requests") + void requestToInstituteEmailConf_shouldMapBatch() { + AuthEmailRequest request = new AuthEmailRequest(); + request.setStateID(29); + + List mapped = mapper.requestToInstituteEmailConf(List.of(request)); + + assertEquals(1, mapped.size()); + assertEquals(29, mapped.get(0).getStateID()); + } + + @Test + @DisplayName("createRequestToInstituteEmailConf should carry every contact field onto the new record") + void createRequestToInstituteEmailConf_shouldCarryContactFields() { + AuthorityEmail mapped = mapper.createRequestToInstituteEmailConf(createAuthorityRequest()); + + assertEquals("Dr Asha Rao", mapped.getAuthorityName()); + assertEquals("asha.rao@example.org", mapped.getEmailID()); + assertEquals("9000000001", mapped.getContactNo()); + assertEquals("admin", mapped.getCreatedBy()); + } + + @Test + @DisplayName("createRequestToInstituteEmailConfig should carry the nodal officer's mobile number too") + void createRequestToInstituteEmailConfig_shouldCarryMobileNumber() { + AuthorityEmail mapped = mapper.createRequestToInstituteEmailConfig(createNodalRequest()); + + assertEquals("Dr Asha Rao", mapped.getAuthorityName()); + assertEquals("asha.rao@example.org", mapped.getEmailID()); + } + + @Test + @DisplayName("the create mappers should each map a whole batch") + void createMappers_shouldMapBatches() { + assertEquals(1, mapper.createRequestToInstituteEmailConf(List.of(createAuthorityRequest())).size()); + assertEquals(1, mapper.createRequestToInstituteEmailConfig(List.of(createNodalRequest())).size()); + } + + @Test + @DisplayName("updateRequestToInstituteEmailConf should carry the edited fields onto the record") + void updateRequestToInstituteEmailConf_shouldCarryEditedFields() { + UpdateAuthEmailRequest request = new UpdateAuthEmailRequest(); + request.setAuthorityEmailID(9001); + request.setAuthorityName("Dr Ravi Kumar"); + request.setEmailID("ravi.kumar@example.org"); + request.setContactNo("9000000003"); + request.setModifiedBy("admin"); + request.setDeleted(Boolean.FALSE); + + AuthorityEmail mapped = mapper.updateRequestToInstituteEmailConf(request); + + assertEquals(9001, mapped.getAuthorityEmailID()); + assertEquals("Dr Ravi Kumar", mapped.getAuthorityName()); + assertEquals("admin", mapped.getModifiedBy()); + } + + @Test + @DisplayName("updateRequestToInstituteNodalEmailConf should carry the edited nodal fields onto the record") + void updateRequestToInstituteNodalEmailConf_shouldCarryEditedFields() { + UpdateNodalEmailRequest request = new UpdateNodalEmailRequest(); + request.setAuthorityEmailID(9001); + request.setAuthorityName("Dr Ravi Kumar"); + request.setMobileNo("9000000004"); + request.setModifiedBy("admin"); + + AuthorityEmail mapped = mapper.updateRequestToInstituteNodalEmailConf(request); + + assertEquals(9001, mapped.getAuthorityEmailID()); + assertEquals("Dr Ravi Kumar", mapped.getAuthorityName()); + } + + @Test + @DisplayName("updateRequestToInstituteEmailConf should map a whole batch of edits") + void updateRequestToInstituteEmailConf_shouldMapBatch() { + UpdateAuthEmailRequest request = new UpdateAuthEmailRequest(); + request.setAuthorityEmailID(9001); + + assertEquals(1, mapper.updateRequestToInstituteEmailConf(List.of(request)).size()); + } + + @Test + @DisplayName("resultToInstTypeEmailResponse should publish the stored record back to the caller") + void resultToInstTypeEmailResponse_shouldPublishStoredRecord() { + AuthEmailResponse published = mapper.resultToInstTypeEmailResponse(storedRecord()); + + assertEquals(9001, published.getAuthorityEmailID()); + assertEquals("Dr Asha Rao", published.getAuthorityName()); + assertEquals("asha.rao@example.org", published.getEmailID()); + assertEquals(Timestamp.valueOf("2026-02-17 09:30:00"), published.getCreatedDate()); + } + + @Test + @DisplayName("the nodal response mappers should publish the stored record back to the caller") + void nodalResponseMappers_shouldPublishStoredRecord() { + NodalEmailResponse published = mapper.resultToInstTypeNodalEmailResponse(storedRecord()); + NodalEmailResponse alsoPublished = mapper.resultInstType(storedRecord()); + + assertEquals("Dr Asha Rao", published.getAuthorityName()); + assertEquals("Dr Asha Rao", alsoPublished.getAuthorityName()); + assertEquals(4001, published.getProviderServiceMapID()); + } + + @Test + @DisplayName("the response mappers should publish a whole batch of stored records") + void responseMappers_shouldPublishBatches() { + List stored = List.of(storedRecord(), storedRecord()); + + assertEquals(2, mapper.resultToInstTypeEmailResponse(stored).size()); + assertEquals(2, mapper.resultToInstTypeEmailResponses(stored).size()); + } + + @Test + @DisplayName("the mappers should answer nothing rather than an empty record for a missing input") + void mappers_shouldAnswerNothingForMissingInput() { + assertNull(mapper.requestToInstituteEmailConf((AuthEmailRequest) null)); + assertNull(mapper.requestToInstituteEmailConf((List) null)); + assertNull(mapper.createRequestToInstituteEmailConf((CreateAuthEmailRequestModel) null)); + assertNull(mapper.createRequestToInstituteEmailConf((List) null)); + assertNull(mapper.createRequestToInstituteEmailConfig((CreateNodalEmailRequestModel) null)); + assertNull(mapper.createRequestToInstituteEmailConfig((List) null)); + assertNull(mapper.updateRequestToInstituteEmailConf((UpdateAuthEmailRequest) null)); + assertNull(mapper.updateRequestToInstituteEmailConf((List) null)); + assertNull(mapper.updateRequestToInstituteNodalEmailConf(null)); + assertNull(mapper.resultToInstTypeEmailResponse((AuthorityEmail) null)); + assertNull(mapper.resultToInstTypeEmailResponse((List) null)); + assertNull(mapper.resultToInstTypeNodalEmailResponse(null)); + assertNull(mapper.resultInstType(null)); + assertNull(mapper.resultToInstTypeEmailResponses(null)); + } + + @Test + @DisplayName("the batch mappers should answer an empty batch for an empty input") + void batchMappers_shouldAnswerEmptyBatchForEmptyInput() { + assertTrue(mapper.requestToInstituteEmailConf(List.of()).isEmpty()); + assertTrue(mapper.resultToInstTypeEmailResponse(List.of()).isEmpty()); + assertTrue(mapper.resultToInstTypeEmailResponses(List.of()).isEmpty()); + } +} diff --git a/src/test/java/com/iemr/admin/mapper/parkingplacetalukmapping/ParkingPlaceTalukMappingMapperTest.java b/src/test/java/com/iemr/admin/mapper/parkingplacetalukmapping/ParkingPlaceTalukMappingMapperTest.java new file mode 100644 index 0000000..6b63657 --- /dev/null +++ b/src/test/java/com/iemr/admin/mapper/parkingplacetalukmapping/ParkingPlaceTalukMappingMapperTest.java @@ -0,0 +1,134 @@ +/* +* 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.admin.mapper.parkingplacetalukmapping; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.admin.data.locationmaster.DistrictBlock; +import com.iemr.admin.data.locationmaster.M_District; +import com.iemr.admin.data.parkingPlace.M_Parkingplace; +import com.iemr.admin.data.parkingPlace.ParkingplaceTalukMapping; +import com.iemr.admin.data.parkingPlace.ParkingplaceTalukMappingTO; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The mapper flattens a stored taluk mapping and the three records it points at + * into the single carrier the parking place screens read. + */ +@DisplayName("ParkingPlaceTalukMappingMapper Test Suite") +class ParkingPlaceTalukMappingMapperTest { + + private final ParkingPlaceTalukMappingMapper mapper = ParkingPlaceTalukMappingMapper.INSTANCE; + + private static ParkingplaceTalukMapping fullMapping() { + ParkingplaceTalukMapping mapping = new ParkingplaceTalukMapping(); + mapping.setPpSubDistrictMapID(7001); + mapping.setParkingPlaceID(31); + mapping.setDistrictID(301); + mapping.setDistrictBlockID(3011); + mapping.setProviderServiceMapID(4001); + mapping.setDeleted(Boolean.FALSE); + mapping.setProcessed("N"); + mapping.setCreatedBy("admin"); + mapping.setModifiedBy("supervisor"); + + M_Parkingplace parkingplace = new M_Parkingplace(); + parkingplace.setParkingPlaceName("Hosur parking"); + parkingplace.setDeleted(Boolean.FALSE); + mapping.setParkingplace(parkingplace); + + M_District district = new M_District(); + district.setDistrictName("Bengaluru Urban"); + district.setDeleted(Boolean.FALSE); + mapping.setM_district(district); + + DistrictBlock block = new DistrictBlock(); + block.setBlockName("Anekal"); + block.setDeleted(Boolean.FALSE); + mapping.setDistrictBlock(block); + return mapping; + } + + @Test + @DisplayName("should carry the names of the parking place, district and taluk onto one carrier") + void shouldFlattenNamesOntoOneCarrier() { + ParkingplaceTalukMappingTO published = mapper.getParkingplaceTalukMappingMap(fullMapping()); + + assertEquals(7001, published.getPpSubDistrictMapID()); + assertEquals("Hosur parking", published.getParkingPlaceName()); + assertEquals("Bengaluru Urban", published.getDistrictName()); + assertEquals("Anekal", published.getDistrictBlockName()); + assertEquals(4001, published.getProviderServiceMapID()); + assertEquals("admin", published.getCreatedBy()); + assertEquals("supervisor", published.getModifiedBy()); + assertEquals("N", published.getProcessed()); + assertEquals(Boolean.FALSE, published.getDeleted()); + assertEquals(Boolean.FALSE, published.getParkingPlaceDeleted()); + assertEquals(Boolean.FALSE, published.getDistrictDeleted()); + assertEquals(Boolean.FALSE, published.getDistrictBlockDeleted()); + } + + @Test + @DisplayName("should leave the names empty when the mapping points at nothing") + void shouldLeaveNamesEmptyWhenNothingPointedAt() { + ParkingplaceTalukMapping mapping = new ParkingplaceTalukMapping(); + mapping.setPpSubDistrictMapID(7002); + + ParkingplaceTalukMappingTO published = mapper.getParkingplaceTalukMappingMap(mapping); + + assertEquals(7002, published.getPpSubDistrictMapID()); + assertNull(published.getParkingPlaceName()); + assertNull(published.getDistrictName()); + assertNull(published.getDistrictBlockName()); + assertNull(published.getParkingPlaceDeleted()); + } + + @Test + @DisplayName("should answer nothing for a mapping that is not there at all") + void shouldAnswerNothingForAbsentMapping() { + assertNull(mapper.getParkingplaceTalukMappingMap(null)); + assertNull(mapper.getParkingplaceTalukMappingMapList(null)); + } + + @Test + @DisplayName("should publish one carrier per mapping in the list") + void shouldPublishOneCarrierPerMapping() { + List published = mapper + .getParkingplaceTalukMappingMapList(List.of(fullMapping(), fullMapping())); + + assertEquals(2, published.size()); + assertEquals("Anekal", published.get(0).getDistrictBlockName()); + } + + @Test + @DisplayName("should publish an empty list when there is no mapping to publish") + void shouldPublishEmptyListForNoMappings() { + assertTrue(mapper.getParkingplaceTalukMappingMapList(new ArrayList<>()).isEmpty()); + } +} diff --git a/src/test/java/com/iemr/admin/service/apiman/ApimanServiceImplTest.java b/src/test/java/com/iemr/admin/service/apiman/ApimanServiceImplTest.java new file mode 100644 index 0000000..98e4d15 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/apiman/ApimanServiceImplTest.java @@ -0,0 +1,261 @@ +/* +* 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.admin.service.apiman; + +import java.util.HashMap; +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.admin.data.apiman.ApimanClient; +import com.iemr.admin.data.apiman.ApimanRegister; +import com.iemr.admin.utils.http.HttpUtils; + +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.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The apiman service registers a new service line as a client of the API + * gateway and signs it up to the API contracts that service line needs. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ApimanServiceImpl Test Suite") +class ApimanServiceImplTest { + + private static final String BASE_URL = "https://gateway.example.org"; + private static final String CLIENT_ID = "client-104"; + + @Mock + private HttpUtils httpUtils; + + private HttpUtils originalHttpUtils; + + private ApimanServiceImpl service; + + @BeforeEach + @DisplayName("Stand in for the gateway and point the service at stub addresses") + void setUp() { + originalHttpUtils = (HttpUtils) ReflectionTestUtils.getField(ApimanServiceImpl.class, "httpUtils"); + ReflectionTestUtils.setField(ApimanServiceImpl.class, "httpUtils", httpUtils); + service = new ApimanServiceImpl(); + ReflectionTestUtils.setField(service, "apimanBaseURL", BASE_URL); + ReflectionTestUtils.setField(service, "clientURL", "APIMAN_URL/clients"); + ReflectionTestUtils.setField(service, "contractURL", "APIMAN_URL/clients/CLIENT_ID/contracts"); + ReflectionTestUtils.setField(service, "registerURL", "APIMAN_URL/register"); + ReflectionTestUtils.setField(service, "getClientKey", "APIMAN_URL/clients/CLIENT_ID/apikey"); + ReflectionTestUtils.setField(service, "auth", "Bearer gateway-token"); + ReflectionTestUtils.setField(service, "apimanplanID", "plan"); + ReflectionTestUtils.setField(service, "apimanorgID", "org"); + ReflectionTestUtils.setField(service, "apimanCommonApiID", "common"); + ReflectionTestUtils.setField(service, "apiman1097apiID", "api1097"); + ReflectionTestUtils.setField(service, "apimanMMUapiID", "mmu"); + ReflectionTestUtils.setField(service, "apimanInventoryapiID", "inventory"); + ReflectionTestUtils.setField(service, "apiman104apiID", "api104"); + ReflectionTestUtils.setField(service, "apimanTMapiID", "tm"); + ReflectionTestUtils.setField(service, "apimanSchedulingapiID", "scheduling"); + ReflectionTestUtils.setField(service, "apimanMCTSapiID", "mcts"); + } + + @AfterEach + @DisplayName("Put the real gateway client back so no other suite sees the stand-in") + void tearDown() { + ReflectionTestUtils.setField(ApimanServiceImpl.class, "httpUtils", originalHttpUtils); + } + + private static ApimanClient client() { + ApimanClient client = new ApimanClient(); + client.setId(CLIENT_ID); + client.setName("104 Helpline"); + client.setInitialVersion("1.0"); + return client; + } + + @Test + @DisplayName("createClient should answer the client the gateway registered") + void createClient_shouldAnswerRegisteredClient() throws Exception { + when(httpUtils.post(anyString(), anyString(), any())) + .thenReturn("{\"id\":\"client-104\",\"name\":\"104 Helpline\"}"); + + ApimanClient registered = service.createClient(client()); + + assertEquals(CLIENT_ID, registered.getId()); + assertEquals("104 Helpline", registered.getName()); + } + + @Test + @DisplayName("createClient should send the client to the gateway with the configured credentials") + void createClient_shouldSendWithConfiguredCredentials() throws Exception { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("{\"id\":\"client-104\"}"); + ArgumentCaptor url = ArgumentCaptor.forClass(String.class); + ArgumentCaptor> header = ArgumentCaptor.forClass(HashMap.class); + + service.createClient(client()); + + verify(httpUtils).post(url.capture(), anyString(), header.capture()); + assertEquals(BASE_URL + "/clients", url.getValue()); + assertEquals("Bearer gateway-token", header.getValue().get("Authorization")); + assertEquals("application/json", header.getValue().get("Content-Type")); + } + + @Test + @DisplayName("createClient should give up when the gateway answers something that is not a client") + void createClient_shouldGiveUpOnUnreadableReply() { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("null"); + + assertThrows(RuntimeException.class, () -> service.createClient(client())); + } + + @Test + @DisplayName("createClientContract should sign a helpline service line up to its own API as well as the shared one") + void createClientContract_shouldSignUpHelplineApis() { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("{}"); + + service.createClientContract(1, CLIENT_ID); + + ArgumentCaptor body = ArgumentCaptor.forClass(String.class); + verify(httpUtils, times(3)).post(anyString(), body.capture(), any()); + assertTrue(body.getAllValues().stream().anyMatch(sent -> sent.contains("api1097")), body.getAllValues() + .toString()); + assertTrue(body.getAllValues().stream().filter(sent -> sent.contains("common")).count() == 2, + "both versions of the shared API must be contracted"); + } + + @Test + @DisplayName("createClientContract should sign a mobile unit service line up to its stock APIs too") + void createClientContract_shouldSignUpMobileUnitApis() { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("{}"); + + service.createClientContract(2, CLIENT_ID); + + ArgumentCaptor body = ArgumentCaptor.forClass(String.class); + verify(httpUtils, times(4)).post(anyString(), body.capture(), any()); + List sent = body.getAllValues(); + assertTrue(sent.stream().anyMatch(one -> one.contains("mmu")), sent.toString()); + assertTrue(sent.stream().anyMatch(one -> one.contains("inventory")), sent.toString()); + } + + @Test + @DisplayName("createClientContract should sign a telemedicine service line up to its scheduling API") + void createClientContract_shouldSignUpTelemedicineApis() { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("{}"); + + service.createClientContract(4, CLIENT_ID); + + ArgumentCaptor body = ArgumentCaptor.forClass(String.class); + verify(httpUtils, times(4)).post(anyString(), body.capture(), any()); + assertTrue(body.getAllValues().stream().anyMatch(one -> one.contains("scheduling")), + body.getAllValues().toString()); + } + + @Test + @DisplayName("createClientContract should contract only the shared API for a service line with no API of its own") + void createClientContract_shouldContractOnlySharedApi() { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("{}"); + + service.createClientContract(5, CLIENT_ID); + + verify(httpUtils, times(2)).post(anyString(), anyString(), any()); + } + + @Test + @DisplayName("createClientContract should contract only the shared API for a service line it does not recognise") + void createClientContract_shouldContractOnlySharedApiForUnknownServiceLine() { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("{}"); + + service.createClientContract(99, CLIENT_ID); + + verify(httpUtils, times(2)).post(anyString(), anyString(), any()); + } + + @Test + @DisplayName("createClientContract should address the client whose contracts are being signed") + void createClientContract_shouldAddressTheClient() { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("{}"); + ArgumentCaptor url = ArgumentCaptor.forClass(String.class); + + service.createClientContract(3, CLIENT_ID); + + verify(httpUtils, times(3)).post(url.capture(), anyString(), any()); + assertEquals(BASE_URL + "/clients/" + CLIENT_ID + "/contracts", url.getValue()); + } + + @Test + @DisplayName("registerClient should publish the registration to the gateway") + void registerClient_shouldPublishRegistration() { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("{}"); + ApimanRegister registration = new ApimanRegister(); + registration.setType("client"); + registration.setEntityId(CLIENT_ID); + ArgumentCaptor url = ArgumentCaptor.forClass(String.class); + + assertNull(service.registerClient(registration), "registration answers nothing back to the caller"); + verify(httpUtils).post(url.capture(), anyString(), any()); + assertEquals(BASE_URL + "/register", url.getValue()); + } + + @Test + @DisplayName("getClientKey should answer the API key the gateway issued") + void getClientKey_shouldAnswerIssuedKey() { + when(httpUtils.get(anyString(), any())).thenReturn("{\"apiKey\":\"key-abc-123\"}"); + + assertEquals("key-abc-123", service.getClientKey(CLIENT_ID)); + } + + @Test + @DisplayName("getClientKey should give up when the gateway does not answer a key") + void getClientKey_shouldGiveUpWithoutKey() { + when(httpUtils.get(anyString(), any())).thenReturn("{\"result\":\"unknown client\"}"); + + assertThrows(RuntimeException.class, () -> service.getClientKey(CLIENT_ID)); + } + + @Test + @DisplayName("the gateway should still be reached when no credentials are configured") + void gatewayCalls_shouldStillBeReachedWithoutCredentials() throws Exception { + ReflectionTestUtils.setField(service, "auth", null); + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("{\"id\":\"client-104\"}"); + ArgumentCaptor> header = ArgumentCaptor.forClass(HashMap.class); + + service.createClient(client()); + + verify(httpUtils).post(anyString(), anyString(), header.capture()); + assertNull(header.getValue().get("Authorization"), "no credentials must be sent when none are configured"); + } +} diff --git a/src/test/java/com/iemr/admin/service/blocking/BlockingServiceTest.java b/src/test/java/com/iemr/admin/service/blocking/BlockingServiceTest.java new file mode 100644 index 0000000..39711d1 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/blocking/BlockingServiceTest.java @@ -0,0 +1,309 @@ +/* +* 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.admin.service.blocking; + +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.admin.data.blocking.M_Providerservicemapping_Blocking; +import com.iemr.admin.data.blocking.M_Serviceprovider_Blocking; +import com.iemr.admin.data.blocking.M_Status1; +import com.iemr.admin.data.blocking.T_Providerservicemappingdetail; +import com.iemr.admin.data.blocking.T_Serviceproviderdetail; +import com.iemr.admin.data.blocking.T_Userdetail; +import com.iemr.admin.data.blocking.UserForBlocking; +import com.iemr.admin.data.blocking.V_Showproviderservicemapping; +import com.iemr.admin.repo.blocking.MProviderservicemappingBlockingRepo; +import com.iemr.admin.repo.blocking.MServiceproviderBlockingRepo; +import com.iemr.admin.repo.blocking.MStatusRepo; +import com.iemr.admin.repo.blocking.T_ProviderservicemappingdetailRepo; +import com.iemr.admin.repo.blocking.T_ServiceproviderdetailRepo; +import com.iemr.admin.repo.blocking.T_UserDetailRepo; +import com.iemr.admin.repo.blocking.UserBlockingRepo; +import com.iemr.admin.repo.blocking.V_ShowproviderservicemappingRepo; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The blocking service turns each request to suspend a provider into the right + * repository update, and reports how far a CTI campaign mapping got. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("Blocking_Service Test Suite") +class BlockingServiceTest { + + private static final Integer PROVIDER_ID = 77; + private static final Integer SERVICE_ID = 3; + private static final Integer STATE_ID = 29; + + @Mock + private V_ShowproviderservicemappingRepo v_ShowproviderservicemappingRepo; + + @Mock + private T_UserDetailRepo t_UserDetailRepo; + + @Mock + private MStatusRepo mStatusRepo; + + @Mock + private UserBlockingRepo userBlockingRepo; + + @Mock + private T_ProviderservicemappingdetailRepo t_ProviderservicemappingdetailRepo; + + @Mock + private MProviderservicemappingBlockingRepo mProviderservicemappingBlockingRepo; + + @Mock + private T_ServiceproviderdetailRepo t_ServiceproviderdetailRepo; + + @Mock + private MServiceproviderBlockingRepo mServiceproviderBlockingRepo; + + @InjectMocks + private Blocking_Service service; + + private static M_Providerservicemapping_Blocking mapping(Integer mapId) { + M_Providerservicemapping_Blocking mapping = new M_Providerservicemapping_Blocking(); + mapping.setProviderServiceMapID(mapId); + mapping.setServiceProviderID(PROVIDER_ID); + mapping.setServiceID(SERVICE_ID); + mapping.setStateID(STATE_ID); + mapping.setcTI_CampaignName("104"); + return mapping; + } + + @Test + @DisplayName("getProviderDetailsById should hand back what the repository holds") + void getProviderDetailsById_shouldHandBackRepositoryContents() { + M_Serviceprovider_Blocking stored = new M_Serviceprovider_Blocking(); + when(mServiceproviderBlockingRepo.getProviderDetailsByID(PROVIDER_ID)).thenReturn(stored); + + assertSame(stored, service.getProviderDetailsById(PROVIDER_ID)); + } + + @Test + @DisplayName("blockServiceProvider should answer the provider the repository stored") + void blockServiceProvider_shouldAnswerStoredProvider() { + M_Serviceprovider_Blocking stored = new M_Serviceprovider_Blocking(); + when(mServiceproviderBlockingRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.blockServiceProvider(stored)); + } + + @Test + @DisplayName("saveData should answer the audit row the repository stored") + void saveData_shouldAnswerStoredAuditRow() { + T_Serviceproviderdetail stored = new T_Serviceproviderdetail(); + when(t_ServiceproviderdetailRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.saveData(stored)); + } + + @Test + @DisplayName("the status updates should each reach their own repository query") + void statusUpdates_shouldReachTheirOwnQuery() { + service.blockProviderByService(PROVIDER_ID, STATE_ID, SERVICE_ID, 2); + service.blockProviderByState(PROVIDER_ID, STATE_ID, 2); + service.blockProvider(PROVIDER_ID, 2); + service.blockProviderByProviderIdAndServiceId(PROVIDER_ID, SERVICE_ID, 2); + service.blockUser(3117, 2); + + verify(mProviderservicemappingBlockingRepo).blockProviderByService(PROVIDER_ID, STATE_ID, SERVICE_ID, 2); + verify(mProviderservicemappingBlockingRepo).blockProviderByState(PROVIDER_ID, STATE_ID, 2); + verify(mProviderservicemappingBlockingRepo).blockProvider(PROVIDER_ID, 2); + verify(mProviderservicemappingBlockingRepo) + .blockProviderByProviderIdAndServiceId(PROVIDER_ID, SERVICE_ID, 2); + verify(userBlockingRepo).blockUser(3117, 2); + } + + @Test + @DisplayName("the mapping lookups should each reach their own repository query") + void mappingLookups_shouldReachTheirOwnQuery() { + M_Providerservicemapping_Blocking stored = mapping(4001); + ArrayList storedList = new ArrayList<>(List.of(stored)); + List stateList = List.of(stored); + when(mProviderservicemappingBlockingRepo.getProviderServiceMappingDetails(PROVIDER_ID, STATE_ID, SERVICE_ID)) + .thenReturn(stored); + when(mProviderservicemappingBlockingRepo.getProviderStateMappingDetails(PROVIDER_ID, STATE_ID)) + .thenReturn(stateList); + when(mProviderservicemappingBlockingRepo.getProviderStatus(PROVIDER_ID)).thenReturn(storedList); + when(mProviderservicemappingBlockingRepo.getProviderStatusByProviderAndServiceId(PROVIDER_ID, SERVICE_ID)) + .thenReturn(storedList); + when(mProviderservicemappingBlockingRepo.findByProviderServiceMapID(4001)).thenReturn(stored); + when(mProviderservicemappingBlockingRepo.save(stored)).thenReturn(stored); + when(mProviderservicemappingBlockingRepo.saveAll(anyList())).thenReturn(storedList); + + assertSame(stored, service.getProviderServiceMappingDetails(PROVIDER_ID, STATE_ID, SERVICE_ID)); + assertSame(stateList, service.getProviderStateMappingDetails(PROVIDER_ID, STATE_ID)); + assertSame(storedList, service.getProviderStatus(PROVIDER_ID)); + assertSame(storedList, service.getProviderStatusByProviderAndServiceId(PROVIDER_ID, SERVICE_ID)); + assertSame(stored, service.getDataByProviderServiceMapId(4001)); + assertSame(stored, service.updateProviderData(stored)); + assertSame(storedList, service.AddServiceProvider(new ArrayList<>())); + } + + @Test + @DisplayName("the view lookups should each reach their own repository query") + void viewLookups_shouldReachTheirOwnQuery() { + ArrayList stored = new ArrayList<>(List.of(new V_Showproviderservicemapping())); + when(v_ShowproviderservicemappingRepo.getProviderStatus(PROVIDER_ID)).thenReturn(stored); + when(v_ShowproviderservicemappingRepo.getProviderStatus1(PROVIDER_ID)).thenReturn(stored); + when(v_ShowproviderservicemappingRepo.getProviderServiceMappingDetails1(PROVIDER_ID, STATE_ID, SERVICE_ID)) + .thenReturn(stored); + when(v_ShowproviderservicemappingRepo.getProviderStateMappingDetails(PROVIDER_ID, STATE_ID)) + .thenReturn(stored); + when(v_ShowproviderservicemappingRepo.getProviderStatusByProviderAndServiceId(PROVIDER_ID, SERVICE_ID)) + .thenReturn(stored); + + assertSame(stored, service.getProviderStatus1(PROVIDER_ID)); + assertSame(stored, service.getProviderStatus2(PROVIDER_ID)); + assertSame(stored, service.getProviderServiceMappingDetails2(PROVIDER_ID, STATE_ID, SERVICE_ID)); + assertSame(stored, service.getProviderStateMappingDetails1(PROVIDER_ID, STATE_ID)); + assertSame(stored, service.getProviderStatusByProviderAndServiceId2(PROVIDER_ID, SERVICE_ID)); + } + + @Test + @DisplayName("the audit writes should each reach their own repository") + void auditWrites_shouldReachTheirOwnRepository() { + T_Providerservicemappingdetail detail = new T_Providerservicemappingdetail(); + ArrayList details = new ArrayList<>(List.of(detail)); + T_Userdetail userDetail = new T_Userdetail(); + when(t_ProviderservicemappingdetailRepo.save(detail)).thenReturn(detail); + when(t_ProviderservicemappingdetailRepo.saveAll(anyList())).thenReturn(details); + when(t_UserDetailRepo.save(userDetail)).thenReturn(userDetail); + + assertSame(detail, service.savetpsdData(detail)); + assertSame(details, service.savetpsmd(new ArrayList<>())); + assertSame(userDetail, service.saveUserDetails(userDetail)); + } + + @Test + @DisplayName("getUserDetailByUserId and getStatusData should hand back what the repositories hold") + void userLookups_shouldHandBackRepositoryContents() { + UserForBlocking user = new UserForBlocking(); + ArrayList statuses = new ArrayList<>(List.of(new M_Status1())); + when(userBlockingRepo.getUserDetailByUserId(3117)).thenReturn(user); + when(mStatusRepo.getStatusData()).thenReturn(statuses); + + assertSame(user, service.getUserDetailByUserId(3117)); + assertSame(statuses, service.getStatusData()); + } + + @Test + @DisplayName("getServiceLiensUsingProvider should skip a row the query could not fill") + void getServiceLiensUsingProvider_shouldSkipUnfillableRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { 4001, PROVIDER_ID, SERVICE_ID, "Tele Medicine", Boolean.FALSE }); + when(mProviderservicemappingBlockingRepo.getServiceLiensUsingProvider(PROVIDER_ID)).thenReturn(rows); + + assertEquals(1, service.getServiceLiensUsingProvider(PROVIDER_ID).size()); + } + + @Test + @DisplayName("mapctidata should report a clean run when every mapping is written") + void mapctidata_shouldReportCleanRun() { + when(mProviderservicemappingBlockingRepo.createcitmapping(anyInt(), any())).thenReturn(1); + + assertEquals("Mapping Successful", service.mapctidata(List.of(mapping(4001), mapping(4002)))); + } + + @Test + @DisplayName("mapctidata should report how far it got when a mapping is rejected") + void mapctidata_shouldReportHowFarItGot() { + when(mProviderservicemappingBlockingRepo.createcitmapping(4001, "104")).thenReturn(1); + when(mProviderservicemappingBlockingRepo.createcitmapping(4002, "104")).thenReturn(0); + + String status = service.mapctidata(List.of(mapping(4001), mapping(4002))); + + assertTrue(status.startsWith("Mapping Failed"), status); + assertTrue(status.contains("after 1 entries"), status); + } + + @Test + @DisplayName("getServiceLiensUsingProvider1 should narrow by provider, service and state when all are named") + void getServiceLiensUsingProvider1_shouldNarrowByAllThree() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { 4001, PROVIDER_ID, SERVICE_ID, "Tele Medicine", STATE_ID, "Karnataka", "104", + Boolean.FALSE, "N", Boolean.FALSE }); + when(mProviderservicemappingBlockingRepo.getServiceLiensUsingProvider1(PROVIDER_ID, SERVICE_ID, STATE_ID)) + .thenReturn(rows); + + assertEquals(1, service.getServiceLiensUsingProvider1(mapping(4001)).size()); + } + + @Test + @DisplayName("getServiceLiensUsingProvider1 should narrow by provider and service when no state is named") + void getServiceLiensUsingProvider1_shouldNarrowByProviderAndService() { + M_Providerservicemapping_Blocking request = mapping(4001); + request.setStateID(null); + when(mProviderservicemappingBlockingRepo.getServiceLiensUsingProvider1(PROVIDER_ID, SERVICE_ID)) + .thenReturn(new ArrayList<>()); + + service.getServiceLiensUsingProvider1(request); + + verify(mProviderservicemappingBlockingRepo).getServiceLiensUsingProvider1(PROVIDER_ID, SERVICE_ID); + } + + @Test + @DisplayName("getServiceLiensUsingProvider1 should narrow by provider alone when no service is named") + void getServiceLiensUsingProvider1_shouldNarrowByProviderAlone() { + M_Providerservicemapping_Blocking request = mapping(4001); + request.setStateID(null); + request.setServiceID(null); + when(mProviderservicemappingBlockingRepo.getServiceLiensUsingProvider1(PROVIDER_ID)) + .thenReturn(new ArrayList<>()); + + service.getServiceLiensUsingProvider1(request); + + verify(mProviderservicemappingBlockingRepo).getServiceLiensUsingProvider1(PROVIDER_ID); + } + + @Test + @DisplayName("getServiceLiensUsingProvider1 should answer every mapping when the request narrows nothing") + void getServiceLiensUsingProvider1_shouldAnswerEveryMapping() { + M_Providerservicemapping_Blocking request = new M_Providerservicemapping_Blocking(); + when(mProviderservicemappingBlockingRepo.getServiceLiensUsingProvider1()).thenReturn(new ArrayList<>()); + + service.getServiceLiensUsingProvider1(request); + + verify(mProviderservicemappingBlockingRepo).getServiceLiensUsingProvider1(); + } +} diff --git a/src/test/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImplTest.java b/src/test/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImplTest.java new file mode 100644 index 0000000..50bd3d1 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImplTest.java @@ -0,0 +1,588 @@ +/* +* 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.admin.service.bulkRegistration; + +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.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.admin.data.bulkuser.BulkRegistrationError; +import com.iemr.admin.data.bulkuser.Employee; +import com.iemr.admin.data.bulkuser.EmployeeList; +import com.iemr.admin.data.employeemaster.M_Community; +import com.iemr.admin.data.employeemaster.M_Gender; +import com.iemr.admin.data.employeemaster.M_Religion; +import com.iemr.admin.data.employeemaster.M_Title; +import com.iemr.admin.data.employeemaster.M_User1; +import com.iemr.admin.data.employeemaster.M_Userqualification; +import com.iemr.admin.data.locationmaster.M_District; +import com.iemr.admin.data.rolemaster.StateMasterForRole; +import com.iemr.admin.repo.employeemaster.V_ShowuserRepo; +import com.iemr.admin.service.employeemaster.EmployeeMasterInter; +import com.iemr.admin.service.locationmaster.LocationMasterServiceInter; +import com.iemr.admin.service.rolemaster.Role_MasterInter; + +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.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Bulk registration turns an uploaded spreadsheet into user records. A row that + * fails validation must be reported rather than half-saved, so the error log is + * as much a deliverable as the users it creates. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("BulkRegistrationServiceImpl Test Suite") +class BulkRegistrationServiceImplTest { + + /** Excel serial numbers, which is how the uploaded sheet carries its dates. */ + private static final String DOB_SERIAL = "30000"; + private static final String DOJ_SERIAL = "40000"; + + @Mock + private EmployeeMasterInter employeeMasterInter; + + @Mock + private Role_MasterInter roleMasterInter; + + @Mock + private V_ShowuserRepo showuserRepo; + + @Mock + private LocationMasterServiceInter locationMasterServiceInter; + + @Mock + private EmployeeXmlService employeeXmlService; + + @InjectMocks + private BulkRegistrationServiceImpl service; + + private Employee employee; + + @BeforeEach + void setUp() throws Exception { + employee = validEmployee(); + + M_Title title = new M_Title(); + title.setTitleID(1); + title.setTitleName("Ms."); + when(employeeMasterInter.getAllTitle()).thenReturn(new ArrayList<>(List.of(title))); + + M_Gender gender = new M_Gender(); + gender.setGenderID(2); + gender.setGenderName("Female"); + when(employeeMasterInter.getAllGender()).thenReturn(new ArrayList<>(List.of(gender))); + + M_Userqualification qualification = new M_Userqualification(); + qualification.setQualificationID(5); + qualification.setName("MBBS"); + when(employeeMasterInter.getQualification()).thenReturn(new ArrayList<>(List.of(qualification))); + + M_Community community = new M_Community(); + community.setCommunityID(3); + community.setCommunityType("General"); + when(employeeMasterInter.getAllCommunity()).thenReturn(new ArrayList<>(List.of(community))); + + M_Religion religion = new M_Religion(); + religion.setReligionID(4); + religion.setReligionType("Hindu"); + when(employeeMasterInter.getAllReligion()).thenReturn(new ArrayList<>(List.of(religion))); + + StateMasterForRole state = new StateMasterForRole(); + state.setStateID(29); + state.setStateName("Karnataka"); + when(roleMasterInter.getAllState()).thenReturn(new ArrayList<>(List.of(state))); + + M_District district = new M_District(); + district.setDistrictID(301); + district.setDistrictName("Bengaluru Urban"); + when(locationMasterServiceInter.getAllDistrictByStateId(29)).thenReturn(new ArrayList<>(List.of(district))); + + when(employeeMasterInter.FindEmployeeName(anyString())).thenReturn("usernotexist"); + when(employeeMasterInter.FindEmployeeContact(anyString())).thenReturn("contactnotexist"); + when(employeeMasterInter.FindEmployeeAadhaar(anyString())).thenReturn("aadhaarnotexist"); + + M_User1 saved = new M_User1(); + saved.setUserID(3117); + when(employeeMasterInter.saveBulkUserEmployee(any())).thenReturn(saved); + } + + private static Employee validEmployee() { + Employee employee = new Employee(); + employee.setTitle("Ms"); + employee.setFirstName("Asha"); + employee.setMiddleName(""); + employee.setLastName("Rao"); + employee.setGender("Female"); + employee.setContactNo("9000000001"); + employee.setDesignation("ASHA"); + employee.setEmergencyContactNo("9000000002"); + employee.setDob(DOB_SERIAL); + employee.setEmail("asha.rao@example.org"); + employee.setAadhaarNo("111122223333"); + employee.setPan("ABCDEFGH123"); + employee.setQualification("MBBS"); + employee.setFatherName("Ravi"); + employee.setMotherName("Meera"); + employee.setCommunity("General"); + employee.setReligion("Hindu"); + employee.setAddressLine1("Main Road"); + employee.setState("Karnataka"); + employee.setDistrict("Bengaluru Urban"); + employee.setPincode("560001"); + employee.setPermanentAddressLine1("Main Road"); + employee.setPermanentState("Karnataka"); + employee.setPermanentDistrict("Bengaluru Urban"); + employee.setPermanentPincode("560001"); + employee.setDateOfJoining(DOJ_SERIAL); + employee.setUserName("EMP-1"); + employee.setPassword("plain-secret"); + return employee; + } + + private void uploadContains(Employee... employees) throws Exception { + EmployeeList list = new EmployeeList(); + list.setEmployees(new ArrayList<>(List.of(employees))); + when(employeeXmlService.parseXml(anyString())).thenReturn(list); + } + + @Nested + @DisplayName("registerBulkUser") + class RegisterBulkUserTests { + + @Test + @DisplayName("should register a row that passes every rule") + void register_shouldRegisterValidRow() throws Exception { + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertEquals(1, service.totalEmployeeListSize); + assertEquals(1, service.m_bulkUser.size()); + assertTrue(service.errorLogs.isEmpty(), service.errorLogs.toString()); + verify(employeeMasterInter).saveBulkUserEmployee(any()); + verify(employeeMasterInter).saveDemography(any()); + } + + @Test + @DisplayName("should carry the uploaded details onto the user it stores") + void register_shouldCarryDetailsOntoStoredUser() throws Exception { + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + M_User1 stored = service.m_bulkUser.get(0); + assertEquals("Asha", stored.getFirstName()); + assertEquals("9000000001", stored.getUserName(), "the contact number is used as the sign-in name"); + assertEquals("EMP-1", stored.getEmployeeID()); + assertEquals(77, stored.getServiceProviderID()); + assertEquals(2, stored.getStatusID()); + assertFalse("plain-secret".equals(stored.getPassword()), "the password must be hashed before storing"); + } + + @Test + @DisplayName("should report a row that names no user") + void register_shouldReportRowWithoutUserName() throws Exception { + employee.setUserName(""); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertEquals(1, service.errorLogs.size()); + assertTrue(service.errorLogs.get(0).contains("Please Enter UserName"), service.errorLogs.toString()); + verify(employeeMasterInter, never()).saveBulkUserEmployee(any()); + } + + @Test + @DisplayName("should report a row whose user name is already taken") + void register_shouldReportExistingUser() throws Exception { + when(employeeMasterInter.FindEmployeeName(anyString())).thenReturn("userexist"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertTrue(service.errorLogs.get(0).contains("User Already exist"), service.errorLogs.toString()); + } + + @Test + @DisplayName("should report a row whose contact number is already taken") + void register_shouldReportExistingContact() throws Exception { + when(employeeMasterInter.FindEmployeeContact(anyString())).thenReturn("contactexist"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertTrue(service.errorLogs.get(0).contains("Contact No Already exist"), service.errorLogs.toString()); + } + + @Test + @DisplayName("should report every rule a row breaks rather than only the first") + void register_shouldReportEveryBrokenRule() throws Exception { + employee.setTitle(""); + employee.setFirstName(""); + employee.setLastName(""); + employee.setEmail("not-an-email"); + employee.setContactNo("12345"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + String reported = service.errorLogs.get(0); + assertTrue(reported.contains("Title is missing."), reported); + assertTrue(reported.contains("First Name is missing."), reported); + assertTrue(reported.contains("Last Name is missing."), reported); + assertTrue(reported.contains("Invalid Email format."), reported); + assertTrue(reported.contains("Contact Number is invalid"), reported); + } + + @Test + @DisplayName("should report a name that is a number rather than a name") + void register_shouldReportNumericName() throws Exception { + employee.setFirstName("12345"); + employee.setLastName("67890"); + employee.setMiddleName("42"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + String reported = service.errorLogs.get(0); + assertTrue(reported.contains("First name is invalid."), reported); + assertTrue(reported.contains("Last name is invalid."), reported); + assertTrue(reported.contains("Middle name is invalid."), reported); + } + + @Test + @DisplayName("should report a name longer than the column can hold") + void register_shouldReportOverlongName() throws Exception { + String tooLong = "A".repeat(51); + employee.setFirstName(tooLong); + employee.setMiddleName(tooLong); + employee.setLastName(tooLong); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + String reported = service.errorLogs.get(0); + assertTrue(reported.contains("First name is invalid."), reported); + assertTrue(reported.contains("Middle name is invalid."), reported); + assertTrue(reported.contains("Last name is invalid."), reported); + } + + @Test + @DisplayName("should report a title the master does not know") + void register_shouldReportUnknownTitle() throws Exception { + employee.setTitle("Archduke"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertTrue(service.errorLogs.get(0).contains("Title is invalid."), service.errorLogs.toString()); + } + + @Test + @DisplayName("should report a district the master does not know") + void register_shouldReportUnknownDistrict() throws Exception { + employee.setDistrict("Nowhere"); + employee.setPermanentDistrict("Nowhere"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + String reported = service.errorLogs.get(0); + assertTrue(reported.contains("Current District is invalid."), reported); + assertTrue(reported.contains("Permanent District is invalid."), reported); + } + + @Test + @DisplayName("should abandon the upload when a row names a state the master does not know") + void register_shouldAbandonUploadForUnknownState() throws Exception { + employee.setState("Atlantis"); + employee.setPermanentState("Atlantis"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertEquals(List.of("Data is invalid or empty"), service.errorLogs, + "an unresolvable state leaves no districts to check against, so the upload is refused"); + assertTrue(service.m_bulkUser.isEmpty()); + } + + @Test + @DisplayName("should report an Aadhaar number that is already on file") + void register_shouldReportDuplicateAadhaar() throws Exception { + when(employeeMasterInter.FindEmployeeAadhaar(anyString())).thenReturn("aadhaarexist"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertTrue(service.errorLogs.get(0).contains("Duplicate aadhaar number found"), + service.errorLogs.toString()); + } + + @Test + @DisplayName("should report an Aadhaar number that is not twelve digits") + void register_shouldReportMalformedAadhaar() throws Exception { + employee.setAadhaarNo("1234"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertTrue(service.errorLogs.get(0).contains("Aadhaar number is invalid"), + service.errorLogs.toString()); + } + + @Test + @DisplayName("should report a date of birth in the future") + void register_shouldReportFutureDateOfBirth() throws Exception { + employee.setDob("50000"); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertTrue(service.errorLogs.get(0).contains("Date of Birth is invalid."), + service.errorLogs.toString()); + } + + @Test + @DisplayName("should report a row whose mandatory fields are simply blank") + void register_shouldReportBlankMandatoryFields() throws Exception { + employee.setGender(""); + employee.setContactNo(""); + employee.setDesignation(""); + employee.setEmergencyContactNo(""); + employee.setDob(""); + employee.setEmail(""); + employee.setPassword(""); + employee.setQualification(""); + employee.setState(""); + employee.setDistrict(""); + employee.setPermanentState(""); + employee.setPermanentDistrict(""); + employee.setDateOfJoining(""); + uploadContains(employee); + + service.registerBulkUser("", "auth", "admin", 77); + + String reported = service.errorLogs.get(0); + assertTrue(reported.contains("Gender is missing"), reported); + assertTrue(reported.contains("Contact number missing"), reported); + assertTrue(reported.contains("Designation is missing"), reported); + assertTrue(reported.contains("Emergency contact number is missing"), reported); + assertTrue(reported.contains("Date of Birth is missing."), reported); + assertTrue(reported.contains("Email is missing."), reported); + assertTrue(reported.contains("Qualification is missing"), reported); + assertTrue(reported.contains("Date of Joining is missing."), reported); + } + + @Test + @DisplayName("should report an upload that carries no rows at all") + void register_shouldReportEmptyUpload() throws Exception { + EmployeeList list = new EmployeeList(); + list.setEmployees(new ArrayList<>()); + when(employeeXmlService.parseXml(anyString())).thenReturn(list); + + service.registerBulkUser("", "auth", "admin", 77); + + assertEquals(List.of("Data is invalid or empty"), service.errorLogs); + } + + @Test + @DisplayName("should report an upload it cannot read at all") + void register_shouldReportUnreadableUpload() throws Exception { + when(employeeXmlService.parseXml(anyString())).thenThrow(new IllegalStateException("not xml")); + + service.registerBulkUser("not xml", "auth", "admin", 77); + + assertEquals(List.of("Data is invalid or empty"), service.errorLogs); + } + + @Test + @DisplayName("should keep going through the sheet after a row it cannot register") + void register_shouldKeepGoingAfterABadRow() throws Exception { + Employee bad = validEmployee(); + bad.setUserName(""); + uploadContains(bad, employee); + + service.registerBulkUser("", "auth", "admin", 77); + + assertEquals(2, service.totalEmployeeListSize); + assertEquals(1, service.m_bulkUser.size(), "the good row must still be registered"); + assertEquals(1, service.errorLogs.size()); + } + } + + @Nested + @DisplayName("Master lookups") + class MasterLookupTests { + + @Test + @DisplayName("getCommunityId should resolve a known community and answer zero for an unknown one") + void getCommunityId_shouldResolveKnownCommunity() { + assertEquals(3, service.getCommunityId("General")); + assertEquals(0, service.getCommunityId("Unknown")); + } + + @Test + @DisplayName("getQualificationId should resolve a known qualification and answer zero for an unknown one") + void getQualificationId_shouldResolveKnownQualification() { + assertEquals(5, service.getQualificationId("MBBS")); + assertEquals(0, service.getQualificationId("Unknown")); + } + + @Test + @DisplayName("getReligionStringId should resolve a known religion") + void getReligionStringId_shouldResolveKnownReligion() { + assertEquals(4, service.getReligionStringId("Hindu")); + assertEquals(0, service.getReligionStringId("Unknown")); + } + + @Test + @DisplayName("getReligionStringId should treat an unstated religion as none") + void getReligionStringId_shouldTreatUnstatedAsNone() { + assertEquals(0, service.getReligionStringId("Not given")); + verify(employeeMasterInter, never()).getAllReligion(); + } + + @Test + @DisplayName("getStateId should resolve a known state and load its districts") + void getStateId_shouldResolveKnownStateAndLoadDistricts() { + assertEquals(29, service.getStateId("Karnataka")); + verify(locationMasterServiceInter).getAllDistrictByStateId(29); + } + + @Test + @DisplayName("getStateId should answer zero for a state the master does not know") + void getStateId_shouldAnswerZeroForUnknownState() { + assertEquals(0, service.getStateId("Atlantis")); + verify(locationMasterServiceInter, never()).getAllDistrictByStateId(29); + } + + @Test + @DisplayName("getDistrictId should resolve a district once its state has been resolved") + void getDistrictId_shouldResolveDistrictAfterState() { + service.getStateId("Karnataka"); + + assertEquals(301, service.getDistrictId("Bengaluru Urban")); + assertEquals(0, service.getDistrictId("Nowhere")); + } + + @Test + @DisplayName("getDistrictId should answer zero when no district name is given") + void getDistrictId_shouldAnswerZeroWithoutAName() { + assertEquals(0, service.getDistrictId("")); + } + + @Test + @DisplayName("getAllState should hand back what the role master holds") + void getAllState_shouldHandBackRoleMasterContents() { + assertEquals(1, service.getAllState().size()); + } + + @Test + @DisplayName("getDesignationId should answer the fixed designation the upload uses") + void getDesignationId_shouldAnswerFixedDesignation() { + assertEquals(20, service.getDesignationId("ASHA")); + } + } + + @Nested + @DisplayName("Helpers") + class HelperTests { + + @Test + @DisplayName("escapeXmlSpecialChars should escape a bare ampersand and leave real entities alone") + void escape_shouldEscapeBareAmpersandOnly() { + assertEquals("Ram & Co", BulkRegistrationServiceImpl.escapeXmlSpecialChars("Ram & Co")); + assertEquals("Ram & Co", BulkRegistrationServiceImpl.escapeXmlSpecialChars("Ram & Co")); + assertEquals("<tag>", BulkRegistrationServiceImpl.escapeXmlSpecialChars("<tag>")); + } + + @Test + @DisplayName("isNumeric should tell a number apart from a name") + void isNumeric_shouldTellNumberFromName() { + assertTrue(BulkRegistrationServiceImpl.isNumeric("12345")); + assertFalse(BulkRegistrationServiceImpl.isNumeric("Asha")); + } + + @Test + @DisplayName("isValidAadhar should report anything that is not twelve digits") + void isValidAadhar_shouldReportNonTwelveDigitNumbers() { + assertFalse(BulkRegistrationServiceImpl.isValidAadhar("111122223333")); + assertTrue(BulkRegistrationServiceImpl.isValidAadhar("1234")); + assertTrue(BulkRegistrationServiceImpl.isValidAadhar("not-a-number")); + } + + @Test + @DisplayName("convertStringIntoDate should read the spreadsheet's own date serial") + void convertStringIntoDate_shouldReadExcelSerial() { + assertEquals("1982-02-18", BulkRegistrationServiceImpl.convertStringIntoDate(DOB_SERIAL).toString()); + } + + @Test + @DisplayName("generateStrongPassword should answer a different hash each time it is called") + void generateStrongPassword_shouldSaltEachHash() throws Exception { + String first = service.generateStrongPassword("plain-secret"); + String second = service.generateStrongPassword("plain-secret"); + + assertTrue(first.startsWith("1001:"), first); + assertFalse(first.equals(second), "each hash must carry its own salt"); + } + + @Test + @DisplayName("insertErrorLog should write one workbook row per reported row") + void insertErrorLog_shouldWriteOneRowPerReportedRow() { + BulkRegistrationError error = new BulkRegistrationError(); + error.setRowNumber(1); + error.setUserName("EMP-1"); + error.setError(List.of("Title is missing.")); + service.bulkRegistrationErrors.add(error); + + byte[] workbook = service.insertErrorLog(); + + assertNotNull(workbook); + assertTrue(workbook.length > 0, "a workbook with a reported row must not be empty"); + } + + @Test + @DisplayName("insertErrorLog should still answer a workbook when nothing was reported") + void insertErrorLog_shouldAnswerWorkbookWithoutErrors() { + assertTrue(service.insertErrorLog().length > 0); + } + } +} diff --git a/src/test/java/com/iemr/admin/service/bulkRegistration/EmployeeXmlServiceTest.java b/src/test/java/com/iemr/admin/service/bulkRegistration/EmployeeXmlServiceTest.java new file mode 100644 index 0000000..cce6537 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/bulkRegistration/EmployeeXmlServiceTest.java @@ -0,0 +1,58 @@ +/* +* 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.admin.service.bulkRegistration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.admin.data.bulkuser.EmployeeList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** Reads the uploaded spreadsheet, which reaches the service as XML. */ +@DisplayName("EmployeeXmlService Test Suite") +class EmployeeXmlServiceTest { + + private final EmployeeXmlService service = new EmployeeXmlService(); + + @Test + @DisplayName("parseXml should read each employee row out of the uploaded document") + void parseXml_shouldReadEachEmployeeRow() throws Exception { + String xml = "AshaRao" + + "EMP-1" + + "RaviKumar" + + "EMP-2"; + + EmployeeList list = service.parseXml(xml); + + assertEquals(2, list.getEmployees().size()); + assertEquals("Asha", list.getEmployees().get(0).getFirstName()); + assertEquals("EMP-2", list.getEmployees().get(1).getUserName()); + } + + @Test + @DisplayName("parseXml should raise rather than answer a half-read document") + void parseXml_shouldRaiseForMalformedDocument() { + assertThrows(Exception.class, () -> service.parseXml("")); + } +} diff --git a/src/test/java/com/iemr/admin/service/calibration/CalibrationServiceImplTest.java b/src/test/java/com/iemr/admin/service/calibration/CalibrationServiceImplTest.java new file mode 100644 index 0000000..79d51be --- /dev/null +++ b/src/test/java/com/iemr/admin/service/calibration/CalibrationServiceImplTest.java @@ -0,0 +1,222 @@ +/* +* 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.admin.service.calibration; + +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.InjectMocks; +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.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.admin.data.calibration.CalibrationStrip; +import com.iemr.admin.repo.calibration.CalibrationRepo; +import com.iemr.admin.utils.exception.IEMRException; +import com.iemr.admin.utils.mapper.OutputMapper; + +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.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The calibration service keeps the test strip codes a provider calibrates + * against, refusing a code the provider already holds. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("CalibrationServiceImpl Test Suite") +class CalibrationServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Long STRIP_ID = 6601L; + private static final String STRIP_CODE = "STRIP-77"; + + @Mock + private CalibrationRepo calibrationRepo; + + @InjectMocks + private CalibrationServiceImpl service; + + @BeforeEach + @DisplayName("Fix the page size the screens are served in and prime the shared output builder") + void setUp() { + ReflectionTestUtils.setField(service, "calibrationPageSize", 10); + new OutputMapper(); + } + + private static CalibrationStrip strip() { + CalibrationStrip strip = new CalibrationStrip(); + strip.setCalibrationStripID(STRIP_ID); + strip.setStripCode(STRIP_CODE); + strip.setProviderServiceMapID(PSM_ID); + strip.setDeleted(Boolean.FALSE); + return strip; + } + + private static CalibrationStrip request() { + CalibrationStrip request = new CalibrationStrip(); + request.setStripCode(STRIP_CODE); + request.setProviderServiceMapID(PSM_ID); + return request; + } + + @Test + @DisplayName("saveData should record a strip code the provider does not hold yet") + void save_shouldRecordNewStripCode() throws Exception { + when(calibrationRepo.checkIfAlreadyStripPresent(PSM_ID, STRIP_CODE)).thenReturn(new ArrayList<>()); + when(calibrationRepo.save(any(CalibrationStrip.class))).thenReturn(strip()); + + assertEquals(1, service.saveData(request())); + } + + @Test + @DisplayName("saveData should refuse a strip code the provider already holds") + void save_shouldRefuseDuplicateStripCode() { + when(calibrationRepo.checkIfAlreadyStripPresent(PSM_ID, STRIP_CODE)) + .thenReturn(new ArrayList<>(List.of(strip()))); + + IEMRException refusal = assertThrows(IEMRException.class, () -> service.saveData(request())); + + assertEquals("Strip code already exists", refusal.getMessage()); + verify(calibrationRepo, never()).save(any(CalibrationStrip.class)); + } + + @Test + @DisplayName("saveData should refuse a strip the repository did not give an identity") + void save_shouldRefuseStripWithoutIdentity() { + when(calibrationRepo.checkIfAlreadyStripPresent(PSM_ID, STRIP_CODE)).thenReturn(new ArrayList<>()); + when(calibrationRepo.save(any(CalibrationStrip.class))).thenReturn(new CalibrationStrip()); + + assertEquals("Error while saving data", + assertThrows(IEMRException.class, () -> service.saveData(request())).getMessage()); + } + + @Test + @DisplayName("saveData should record nothing when the request names no strip code") + void save_shouldRecordNothingWithoutStripCode() throws Exception { + assertEquals(0, service.saveData(new CalibrationStrip())); + verify(calibrationRepo, never()).save(any(CalibrationStrip.class)); + } + + @Test + @DisplayName("fetchData should answer one page of strips and how many pages there are") + void fetch_shouldAnswerOnePageAndPageCount() throws Exception { + CalibrationStrip request = request(); + request.setPageNo(0); + Pageable pageable = PageRequest.of(0, 10); + Page page = new PageImpl<>(List.of(strip()), pageable, 1); + when(calibrationRepo.getCalibrationStripsWithPagination(PSM_ID, pageable)).thenReturn(page); + + String answered = service.fetchData(request); + + assertTrue(answered.contains(STRIP_CODE), answered); + assertTrue(answered.contains("pageCount"), answered); + } + + @Test + @DisplayName("fetchData should answer every strip when the caller asks for no particular page") + void fetch_shouldAnswerEveryStripWithoutPaging() throws Exception { + when(calibrationRepo.getCalibrationStripsWithoutPagination(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(strip()))); + + String answered = service.fetchData(request()); + + assertTrue(answered.contains(STRIP_CODE), answered); + assertTrue(!answered.contains("pageCount"), "an unpaged answer carries no page count"); + } + + @Test + @DisplayName("fetchData should refuse a request that names no provider") + void fetch_shouldRefuseRequestWithoutProvider() { + assertThrows(IEMRException.class, () -> service.fetchData(new CalibrationStrip())); + } + + @Test + @DisplayName("deleteData should report how many strips the retirement touched") + void delete_shouldReportRowsTouched() throws Exception { + CalibrationStrip request = strip(); + request.setDeleted(Boolean.TRUE); + when(calibrationRepo.deleteCalibrationStrip(STRIP_ID, Boolean.TRUE)).thenReturn(1); + + assertEquals(1, service.deleteData(request)); + } + + @Test + @DisplayName("deleteData should refuse a request that names no strip") + void delete_shouldRefuseRequestWithoutStrip() { + assertEquals("Invalid request", + assertThrows(IEMRException.class, () -> service.deleteData(new CalibrationStrip())).getMessage()); + } + + @Test + @DisplayName("deleteData should give up when the retirement cannot be recorded") + void delete_shouldGiveUpWhenStorageFails() { + CalibrationStrip request = strip(); + request.setDeleted(Boolean.TRUE); + when(calibrationRepo.deleteCalibrationStrip(anyLong(), anyBoolean())) + .thenThrow(new RuntimeException("row is locked")); + + assertThrows(IEMRException.class, () -> service.deleteData(request)); + } + + @Test + @DisplayName("updateData should record the change against the strip") + void update_shouldRecordChange() throws Exception { + when(calibrationRepo.save(any(CalibrationStrip.class))).thenReturn(strip()); + + assertEquals(1, service.updateData(request())); + } + + @Test + @DisplayName("updateData should refuse a strip the repository did not give an identity") + void update_shouldRefuseStripWithoutIdentity() { + when(calibrationRepo.save(any(CalibrationStrip.class))).thenReturn(new CalibrationStrip()); + + assertEquals("Error while updating data", + assertThrows(IEMRException.class, () -> service.updateData(request())).getMessage()); + } + + @Test + @DisplayName("updateData should record nothing when the request names no strip code") + void update_shouldRecordNothingWithoutStripCode() throws Exception { + assertEquals(0, service.updateData(new CalibrationStrip())); + } +} diff --git a/src/test/java/com/iemr/admin/service/drugstrangth/DrugStrangthServiceTest.java b/src/test/java/com/iemr/admin/service/drugstrangth/DrugStrangthServiceTest.java new file mode 100644 index 0000000..b8b1632 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/drugstrangth/DrugStrangthServiceTest.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.admin.service.drugstrangth; + +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.admin.data.drugstrangth.M_104DrugStrength; +import com.iemr.admin.repo.blocking.DrugStrangthRepo; + +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.anyList; +import static org.mockito.Mockito.when; + +/** + * The drug strength service keeps the strengths a drug can be dispensed in. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("DrugStrangthService Test Suite") +class DrugStrangthServiceTest { + + private static final Integer STRENGTH_ID = 33; + + @Mock + private DrugStrangthRepo drugStrangthRepo; + + @InjectMocks + private DrugStrangthService service; + + private static M_104DrugStrength strength() { + M_104DrugStrength strength = new M_104DrugStrength(); + strength.setDrugStrengthID(STRENGTH_ID); + strength.setDrugStrength("500 mg"); + return strength; + } + + @Test + @DisplayName("createDrugStrangth should answer the strengths the repository stored") + void create_shouldAnswerStoredStrengths() { + ArrayList stored = new ArrayList<>(List.of(strength())); + when(drugStrangthRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.createDrugStrangth(new ArrayList<>())); + } + + @Test + @DisplayName("getDrugStrangth should answer every strength on file") + void get_shouldAnswerEveryStrength() { + when(drugStrangthRepo.findAll()).thenReturn(new ArrayList<>(List.of(strength()))); + + assertEquals(1, service.getDrugStrangth().size()); + } + + @Test + @DisplayName("getDrugStrangth should answer nothing when no strength is on file") + void get_shouldAnswerNothingWhenNoneOnFile() { + when(drugStrangthRepo.findAll()).thenReturn(new ArrayList()); + + assertTrue(service.getDrugStrangth().isEmpty()); + } + + @Test + @DisplayName("updateDrugStrangth and saveupdatedData should each reach their own repository query") + void singleRecordOperations_shouldReachTheirOwnQuery() { + M_104DrugStrength stored = strength(); + when(drugStrangthRepo.findByDrugStrengthID(STRENGTH_ID)).thenReturn(stored); + when(drugStrangthRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.updateDrugStrangth(STRENGTH_ID)); + assertSame(stored, service.saveupdatedData(stored)); + } + + @Test + @DisplayName("updateDrugStrangth should answer nothing when the strength is unknown") + void update_shouldAnswerNothingForUnknownStrength() { + when(drugStrangthRepo.findByDrugStrengthID(-1)).thenReturn(null); + + assertNull(service.updateDrugStrangth(-1)); + } +} diff --git a/src/test/java/com/iemr/admin/service/drugtype/DrugtypeServiceImplTest.java b/src/test/java/com/iemr/admin/service/drugtype/DrugtypeServiceImplTest.java new file mode 100644 index 0000000..bfdfebc --- /dev/null +++ b/src/test/java/com/iemr/admin/service/drugtype/DrugtypeServiceImplTest.java @@ -0,0 +1,113 @@ +/* +* 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.admin.service.drugtype; + +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.admin.data.drugtype.M_Drugtype; +import com.iemr.admin.repo.drugtype.DrugtypeRepo; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.when; + +/** + * The drug type service keeps the dosage forms a provider stocks. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("DrugtypeServiceImpl Test Suite") +class DrugtypeServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer DRUG_TYPE_ID = 21; + + @Mock + private DrugtypeRepo drugtypeRepo; + + @InjectMocks + private DrugtypeServiceImpl service; + + private static M_Drugtype drugType() { + M_Drugtype drugType = new M_Drugtype(); + drugType.setDrugTypeID(DRUG_TYPE_ID); + drugType.setDrugTypeName("Tablet"); + drugType.setProviderServiceMapID(PSM_ID); + return drugType; + } + + @Test + @DisplayName("createDrugtypeData should answer the drug types the repository stored") + void create_shouldAnswerStoredDrugTypes() { + ArrayList stored = new ArrayList<>(List.of(drugType())); + when(drugtypeRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.createDrugtypeData(new ArrayList<>())); + } + + @Test + @DisplayName("createDrugtypeData should answer nothing when the repository stored nothing") + void create_shouldAnswerNothingWhenNothingStored() { + when(drugtypeRepo.saveAll(anyList())).thenReturn(new ArrayList()); + + assertNull(service.createDrugtypeData(new ArrayList<>())); + } + + @Test + @DisplayName("getDrugtypeData should answer the drug types the provider stocks") + void get_shouldAnswerProvidersDrugTypes() { + ArrayList stocked = new ArrayList<>(List.of(drugType())); + when(drugtypeRepo.getDrugtypeData(PSM_ID)).thenReturn(stocked); + + assertSame(stocked, service.getDrugtypeData(PSM_ID)); + } + + @Test + @DisplayName("editDrugtypeData and saveeditDrugtype should each reach their own repository query") + void singleRecordOperations_shouldReachTheirOwnQuery() { + M_Drugtype stored = drugType(); + when(drugtypeRepo.geteditedData(DRUG_TYPE_ID)).thenReturn(stored); + when(drugtypeRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.editDrugtypeData(DRUG_TYPE_ID)); + assertSame(stored, service.saveeditDrugtype(stored)); + } + + @Test + @DisplayName("editDrugtypeData should answer nothing when the drug type is unknown") + void edit_shouldAnswerNothingForUnknownDrugType() { + when(drugtypeRepo.geteditedData(-1)).thenReturn(null); + + assertNull(service.editDrugtypeData(-1)); + } +} diff --git a/src/test/java/com/iemr/admin/service/emailconfig/EmailConfigServiceImplTest.java b/src/test/java/com/iemr/admin/service/emailconfig/EmailConfigServiceImplTest.java new file mode 100644 index 0000000..11b5b8b --- /dev/null +++ b/src/test/java/com/iemr/admin/service/emailconfig/EmailConfigServiceImplTest.java @@ -0,0 +1,189 @@ +/* +* 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.admin.service.emailconfig; + +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.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.admin.data.emailconfig.AuthorityEmail; +import com.iemr.admin.mapper.emailconfig.InstituteEmailConfigMapper; +import com.iemr.admin.model.emailconfig.AuthEmailRequest; +import com.iemr.admin.model.emailconfig.AuthEmailResponse; +import com.iemr.admin.model.emailconfig.CreateAuthEmailRequestModel; +import com.iemr.admin.model.emailconfig.UpdateAuthEmailRequest; +import com.iemr.admin.repository.emailconfig.InstituteEmailRepo; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The email config service keeps the authority mailboxes a complaint is copied + * to, narrowed by whichever parts of the location the caller names. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("EmailConfigServiceImpl Test Suite") +class EmailConfigServiceImplTest { + + @Mock + private EntityManager entityManager; + + @Mock + private InstituteEmailRepo instituteRepo; + + @Mock + private InstituteEmailConfigMapper instituteEmailConfigMapper; + + @InjectMocks + private EmailConfigServiceImpl service; + + private CriteriaQuery query; + private TypedQuery typedQuery; + + @SuppressWarnings("unchecked") + @BeforeEach + @DisplayName("Stand in for the criteria query the service builds by hand") + void setUp() { + CriteriaBuilder builder = mock(CriteriaBuilder.class); + query = mock(CriteriaQuery.class); + Root root = mock(Root.class); + typedQuery = mock(TypedQuery.class); + + when(entityManager.getCriteriaBuilder()).thenReturn(builder); + when(builder.createQuery(AuthorityEmail.class)).thenReturn(query); + when(query.from(AuthorityEmail.class)).thenReturn(root); + when(query.select(any())).thenReturn(query); + when(query.where(any(Predicate[].class))).thenReturn(query); + when(query.orderBy(any(jakarta.persistence.criteria.Order[].class))).thenReturn(query); + when(root.get(anyString())).thenReturn(mock(Path.class)); + when(builder.equal(any(), any())).thenReturn(mock(Predicate.class)); + when(entityManager.createQuery(query)).thenReturn(typedQuery); + } + + private static AuthEmailRequest fullyNarrowedRequest() { + AuthEmailRequest request = new AuthEmailRequest(); + request.setAuthorityEmailID(1); + request.setDeleted(false); + request.setDistrictID(301); + request.setDistrictBranchMappingID(30111); + request.setBlockID(3011); + request.setProviderServiceMapID(4001); + request.setStateID(29); + return request; + } + + @Test + @DisplayName("getAllEmailConfigs should answer the mailboxes the query found, as the screens read them") + void getAll_shouldAnswerFoundMailboxes() { + List found = List.of(new AuthorityEmail()); + List published = List.of(new AuthEmailResponse()); + when(typedQuery.getResultList()).thenReturn(found); + when(instituteEmailConfigMapper.resultToInstTypeEmailResponse(found)).thenReturn(published); + + assertSame(published, service.getAllEmailConfigs(fullyNarrowedRequest())); + } + + @Test + @DisplayName("getAllEmailConfigs should narrow the query by every detail the caller named") + void getAll_shouldNarrowByEveryNamedDetail() { + when(typedQuery.getResultList()).thenReturn(new ArrayList<>()); + + service.getAllEmailConfigs(fullyNarrowedRequest()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Predicate[].class); + verify(query).where(captor.capture()); + assertEquals(7, captor.getValue().length, "one narrowing per detail the caller named"); + } + + @Test + @DisplayName("getAllEmailConfigs should narrow by nothing when the caller names nothing") + void getAll_shouldNarrowByNothingForEmptyRequest() { + when(typedQuery.getResultList()).thenReturn(new ArrayList<>()); + + service.getAllEmailConfigs(new AuthEmailRequest()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Predicate[].class); + verify(query).where(captor.capture()); + assertEquals(0, captor.getValue().length); + } + + @Test + @DisplayName("saveEmailConfigs should store one mailbox per request and answer each as stored") + void save_shouldStoreEachRequestedMailbox() { + AuthorityEmail stored = new AuthorityEmail(); + when(instituteEmailConfigMapper.createRequestToInstituteEmailConf(anyList())) + .thenReturn(List.of(stored, stored)); + when(instituteRepo.save(stored)).thenReturn(stored); + when(instituteEmailConfigMapper.resultToInstTypeEmailResponse(stored)) + .thenReturn(new AuthEmailResponse()); + + assertEquals(2, service.saveEmailConfigs(List.of(new CreateAuthEmailRequestModel())).size()); + } + + @Test + @DisplayName("saveEmailConfigs should store nothing when the caller asks for nothing") + void save_shouldStoreNothingForEmptyRequest() { + when(instituteEmailConfigMapper.createRequestToInstituteEmailConf(anyList())) + .thenReturn(new ArrayList<>()); + + assertTrue(service.saveEmailConfigs(new ArrayList<>()).isEmpty()); + } + + @Test + @DisplayName("updateEmailConfigs should answer the mailbox as it stands after the change") + void update_shouldAnswerChangedMailbox() { + AuthorityEmail stored = new AuthorityEmail(); + AuthEmailResponse published = new AuthEmailResponse(); + when(instituteEmailConfigMapper.updateRequestToInstituteEmailConf(any(UpdateAuthEmailRequest.class))) + .thenReturn(stored); + when(instituteRepo.save(stored)).thenReturn(stored); + when(instituteEmailConfigMapper.resultToInstTypeEmailResponse(stored)).thenReturn(published); + + assertSame(published, service.updateEmailConfigs(new UpdateAuthEmailRequest())); + } +} diff --git a/src/test/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImplTest.java b/src/test/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImplTest.java new file mode 100644 index 0000000..e70bfee --- /dev/null +++ b/src/test/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImplTest.java @@ -0,0 +1,1195 @@ +/* +* 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.admin.service.employeemaster; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +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.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.admin.data.employeemaster.EmployeeSignature; +import com.iemr.admin.data.employeemaster.M_Community; +import com.iemr.admin.data.employeemaster.M_Gender; +import com.iemr.admin.data.employeemaster.M_ProviderServiceMap1; +import com.iemr.admin.data.employeemaster.M_Religion; +import com.iemr.admin.data.employeemaster.M_Role; +import com.iemr.admin.data.employeemaster.M_Title; +import com.iemr.admin.data.employeemaster.M_User1; +import com.iemr.admin.data.employeemaster.M_UserDemographics; +import com.iemr.admin.data.employeemaster.M_UserLangMapping; +import com.iemr.admin.data.employeemaster.M_UserServiceRoleMapping2; +import com.iemr.admin.data.employeemaster.M_Userqualification; +import com.iemr.admin.data.employeemaster.Showofficedetails1; +import com.iemr.admin.data.employeemaster.Showuserdetailsfromuserservicerolemapping; +import com.iemr.admin.data.employeemaster.V_Showuser; +import com.iemr.admin.data.employeemaster.V_Userservicerolemapping; +import com.iemr.admin.data.facilitytype.M_facilitytype; +import com.iemr.admin.data.rolemaster.M_UserservicerolemappingForRoleProviderAdmin; +import com.iemr.admin.data.rolemaster.UserRole; +import com.iemr.admin.data.store.M_Facility; +import com.iemr.admin.exceptionhandler.DataNotFound; +import com.iemr.admin.repo.employeemaster.EmployeeMasterRepo; +import com.iemr.admin.repo.employeemaster.EmployeeMasterRepoo; +import com.iemr.admin.repo.employeemaster.EmployeeSignatureRepo; +import com.iemr.admin.repo.employeemaster.M_CommunityRepo; +import com.iemr.admin.repo.employeemaster.M_GenderRepo; +import com.iemr.admin.repo.employeemaster.M_ProviderServiceMap1Repo; +import com.iemr.admin.repo.employeemaster.M_QualificationRepo; +import com.iemr.admin.repo.employeemaster.M_ReligionRepo; +import com.iemr.admin.repo.employeemaster.M_TitleRepo; +import com.iemr.admin.repo.employeemaster.M_UserDemographicsRepo; +import com.iemr.admin.repo.employeemaster.M_UserLangMappingRepo; +import com.iemr.admin.repo.employeemaster.RoleRepo; +import com.iemr.admin.repo.employeemaster.Showofficedetails1Repo1; +import com.iemr.admin.repo.employeemaster.ShowuserdetailsfromuserservicerolemappingRepo; +import com.iemr.admin.repo.employeemaster.V_ShowuserRepo; +import com.iemr.admin.repo.employeemaster.V_UserservicerolemappingRepo; +import com.iemr.admin.repository.facilitytype.M_facilitytypeRepo; +import com.iemr.admin.repository.rolemaster.M_UserservicerolemappingForRoleProviderAdminRepo; +import com.iemr.admin.repository.store.MainStoreRepo; +import com.iemr.admin.service.user.EncryptUserPassword; +import com.iemr.admin.utils.response.OutputResponse; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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.assertNull; +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.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; + +/** + * The employee master service is where the rules about who may work where live: + * an ASHA must sit at a sub-centre, a role may not be mapped twice, and taking a + * role away has to take the supervisor mappings with it. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("EmployeeMasterServiceImpl Test Suite") +class EmployeeMasterServiceImplTest { + + private static final Integer USER_ID = 3117; + private static final Integer PSM_ID = 4001; + + @Mock + private RoleRepo roleRepo; + + @Mock + private EmployeeMasterRepo employeeMasterRepo; + + @Mock + private EmployeeMasterRepoo employeeMasterRepo11; + + @Mock + private EmployeeMasterRepoo employeeMasterRepoo; + + @Mock + private M_UserDemographicsRepo m_UserDemographicsRepo; + + @Mock + private M_UserLangMappingRepo m_UserLangMappingRepo; + + @Mock + private M_TitleRepo m_TitleRepo; + + @Mock + private M_GenderRepo m_GenderRepo; + + @Mock + private ShowuserdetailsfromuserservicerolemappingRepo showuserdetailsfromuserservicerolemappingRepo; + + @Mock + private V_ShowuserRepo v_ShowuserRepo; + + @Mock + private V_UserservicerolemappingRepo v_UserservicerolemappingRepo; + + @Mock + private M_QualificationRepo m_QualificationRepo; + + @Mock + private Showofficedetails1Repo1 showofficedetails1Repo1; + + @Mock + private M_ProviderServiceMap1Repo m_ProviderServiceMap1Repo; + + @Mock + private MainStoreRepo mainStoreRepo; + + @Mock + private M_facilitytypeRepo facilityTypeRepo; + + @Mock + private EmployeeSignatureRepo employeeSignatureRepo; + + @Mock + private M_CommunityRepo m_CommunityRepo; + + @Mock + private M_ReligionRepo m_ReligionRepo; + + @Mock + private M_UserservicerolemappingForRoleProviderAdminRepo userservicerolemappingForRoleProviderAdminRepo; + + @Mock + private AshaSupervisorMappingService ashaSupervisorMappingService; + + @Mock + private EncryptUserPassword encryptUserPassword; + + @InjectMocks + private EmployeeMasterServiceImpl service; + + private M_Role role; + + @BeforeEach + void setUp() { + role = new M_Role(); + role.setRoleID(11); + role.setRoleName("Counsellor"); + when(roleRepo.findByRoleID(anyInt())).thenReturn(role); + } + + private static M_UserServiceRoleMapping2 mapping(Integer id, Integer roleId) { + M_UserServiceRoleMapping2 mapping = new M_UserServiceRoleMapping2(); + mapping.setuSRMappingID(id); + mapping.setUserID(USER_ID); + mapping.setRoleID(roleId); + mapping.setProviderServiceMapID(PSM_ID); + return mapping; + } + + private void namedRole(String name) { + role.setRoleName(name); + } + + private void activeFacility(Integer facilityId, Integer typeId, Integer levelValue, Integer maxLevel) { + M_Facility facility = new M_Facility(); + facility.setFacilityID(facilityId); + facility.setFacilityTypeID(typeId); + when(mainStoreRepo.findByFacilityIDAndDeleted(facilityId, false)).thenReturn(facility); + M_facilitytype type = new M_facilitytype(); + type.setFacilityTypeID(typeId); + type.setLevelValue(levelValue); + when(facilityTypeRepo.findByFacilityTypeID(typeId)).thenReturn(type); + when(facilityTypeRepo.findMaxLevelValueByProviderServiceMapID(PSM_ID)).thenReturn(maxLevel); + } + + @Nested + @DisplayName("mapRole") + class MapRoleTests { + + @Test + @DisplayName("should refuse an ASHA mapping that names no facility") + void mapRole_shouldRefuseAshaWithoutFacility() { + namedRole("ASHA"); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.mapRole(List.of(mapping(null, 11)), "auth")); + + assertTrue(thrown.getMessage().contains("Facility (SC) is mandatory for ASHA role"), + thrown.getMessage()); + verify(employeeMasterRepo, never()).saveAll(anyList()); + } + + @Test + @DisplayName("should refuse a mapping onto a facility that has been retired") + void mapRole_shouldRefuseRetiredFacility() { + M_UserServiceRoleMapping2 toMap = mapping(null, 11); + toMap.setFacilityID(501); + when(mainStoreRepo.findByFacilityIDAndDeleted(501, false)).thenReturn(null); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.mapRole(List.of(toMap), "auth")); + + assertTrue(thrown.getMessage().contains("is no longer active"), thrown.getMessage()); + } + + @Test + @DisplayName("should refuse an ASHA mapped above sub-centre level") + void mapRole_shouldRefuseAshaAboveSubCentre() { + namedRole("ASHA"); + M_UserServiceRoleMapping2 toMap = mapping(null, 11); + toMap.setFacilityID(501); + activeFacility(501, 3, 2, 4); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.mapRole(List.of(toMap), "auth")); + + assertTrue(thrown.getMessage().contains("Sub-Centre (SC) level facility"), thrown.getMessage()); + } + + @Test + @DisplayName("should refuse a second active mapping for the same user, role and service line") + void mapRole_shouldRefuseDuplicateMapping() { + when(employeeMasterRepo.existsByUserIDAndRoleIDAndProviderServiceMapIDAndDeletedFalse(USER_ID, 11, PSM_ID)) + .thenReturn(true); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.mapRole(List.of(mapping(null, 11)), "auth")); + + assertTrue(thrown.getMessage().contains("Duplicate mapping is not allowed"), thrown.getMessage()); + } + + @Test + @DisplayName("should refuse a second mapping for an ASHA supervisor at the same facility") + void mapRole_shouldRefuseDuplicateSupervisorMappingAtSameFacility() { + namedRole("ASHA Supervisor"); + M_UserServiceRoleMapping2 toMap = mapping(null, 11); + toMap.setFacilityID(501); + activeFacility(501, 3, 4, 4); + when(employeeMasterRepo + .existsByUserIDAndRoleIDAndProviderServiceMapIDAndFacilityIDAndDeletedFalse( + USER_ID, 11, PSM_ID, 501)) + .thenReturn(true); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.mapRole(List.of(toMap), "auth")); + + assertTrue(thrown.getMessage().contains("active work location mapping for this facility"), + thrown.getMessage()); + } + + @Test + @DisplayName("should let an ASHA supervisor hold a second mapping at a different facility") + void mapRole_shouldAllowSupervisorAtDifferentFacility() throws Exception { + namedRole("ASHA Supervisor"); + M_UserServiceRoleMapping2 toMap = mapping(null, 11); + toMap.setFacilityID(502); + activeFacility(502, 3, 4, 4); + ArrayList stored = new ArrayList<>(List.of(toMap)); + when(employeeMasterRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.mapRole(List.of(toMap), "auth")); + } + + @Test + @DisplayName("should flatten the village lists onto the stored columns") + void mapRole_shouldFlattenVillageLists() throws Exception { + M_UserServiceRoleMapping2 toMap = mapping(null, 11); + toMap.setVillageID(new String[] { "501", "502" }); + toMap.setVillageName(new String[] { "Hosur", "Devanahalli" }); + when(employeeMasterRepo.saveAll(anyList())) + .thenReturn(new ArrayList<>(List.of(toMap))); + + service.mapRole(List.of(toMap), "auth"); + + assertEquals("501,502", toMap.getVillageidDb()); + assertEquals("Hosur,Devanahalli", toMap.getVillageNameDb()); + verify(employeeMasterRepo).save(toMap); + } + + @Test + @DisplayName("should leave the village columns alone when the mapping names no villages") + void mapRole_shouldLeaveVillageColumnsAlone() throws Exception { + M_UserServiceRoleMapping2 toMap = mapping(null, 11); + when(employeeMasterRepo.saveAll(anyList())).thenReturn(new ArrayList<>(List.of(toMap))); + + service.mapRole(List.of(toMap), "auth"); + + assertNull(toMap.getVillageidDb()); + verify(employeeMasterRepo, never()).save(any()); + } + } + + @Nested + @DisplayName("saveRoleMappingeditedData") + class SaveRoleMappingTests { + + @Test + @DisplayName("should refuse an ASHA edit that drops the facility") + void saveRoleMapping_shouldRefuseAshaWithoutFacility() { + namedRole("ASHA"); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.saveRoleMappingeditedData(mapping(9001, 11), "auth")); + + assertTrue(thrown.getMessage().contains("Facility (SC) is mandatory for ASHA role"), + thrown.getMessage()); + } + + @Test + @DisplayName("should skip the facility rules when the mapping is only being deactivated") + void saveRoleMapping_shouldSkipRulesWhenDeactivating() throws Exception { + namedRole("ASHA"); + M_UserServiceRoleMapping2 toSave = mapping(9001, 11); + toSave.setDeleted(Boolean.TRUE); + M_UserServiceRoleMapping2 old = mapping(9001, 11); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.of(old)); + when(employeeMasterRepo.save(toSave)).thenReturn(toSave); + + assertSame(toSave, service.saveRoleMappingeditedData(toSave, "auth")); + verify(ashaSupervisorMappingService).cascadeDeleteByUserID(USER_ID, "Admin"); + } + + @Test + @DisplayName("should clear the saved villages when the facility is above sub-centre level") + void saveRoleMapping_shouldClearVillagesAboveSubCentre() throws Exception { + M_UserServiceRoleMapping2 toSave = mapping(9001, 11); + toSave.setFacilityID(501); + toSave.setVillageidDb("501,502"); + toSave.setVillageNameDb("Hosur,Devanahalli"); + activeFacility(501, 3, 2, 4); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.empty()); + when(employeeMasterRepo.save(toSave)).thenReturn(toSave); + + service.saveRoleMappingeditedData(toSave, "auth"); + + assertNull(toSave.getVillageidDb(), "a non sub-centre posting keeps no village list"); + assertNull(toSave.getVillageNameDb()); + } + + @Test + @DisplayName("should cascade the supervisor mappings when the role changes") + void saveRoleMapping_shouldCascadeWhenRoleChanges() throws Exception { + M_UserServiceRoleMapping2 toSave = mapping(9001, 12); + M_UserServiceRoleMapping2 old = mapping(9001, 11); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.of(old)); + when(employeeMasterRepo.save(toSave)).thenReturn(toSave); + + service.saveRoleMappingeditedData(toSave, "auth"); + + verify(ashaSupervisorMappingService).cascadeDeleteByUserID(USER_ID, "Admin"); + } + + @Test + @DisplayName("should cascade only this facility when a supervisor still works elsewhere") + void saveRoleMapping_shouldCascadeOnlyThisFacilityForBusySupervisor() throws Exception { + namedRole("ASHA Supervisor"); + M_UserServiceRoleMapping2 toSave = mapping(9001, 11); + toSave.setDeleted(Boolean.TRUE); + toSave.setFacilityID(501); + M_UserServiceRoleMapping2 old = mapping(9001, 11); + old.setFacilityID(501); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.of(old)); + when(employeeMasterRepo.countByUserIDAndRoleIDAndDeletedFalse(USER_ID, 11)).thenReturn(2L); + when(employeeMasterRepo.save(toSave)).thenReturn(toSave); + + service.saveRoleMappingeditedData(toSave, "auth"); + + verify(ashaSupervisorMappingService).cascadeDeleteByFacilityID(501, "Admin"); + verify(ashaSupervisorMappingService, never()).cascadeDeleteByUserID(anyInt(), anyString()); + } + + @Test + @DisplayName("should cascade every mapping when the facility itself changes") + void saveRoleMapping_shouldCascadeWhenFacilityChanges() throws Exception { + M_UserServiceRoleMapping2 toSave = mapping(9001, 11); + toSave.setFacilityID(502); + activeFacility(502, 3, 4, 4); + M_UserServiceRoleMapping2 old = mapping(9001, 11); + old.setFacilityID(501); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.of(old)); + when(employeeMasterRepo.save(toSave)).thenReturn(toSave); + + service.saveRoleMappingeditedData(toSave, "auth"); + + verify(ashaSupervisorMappingService).cascadeDeleteByUserID(USER_ID, "Admin"); + } + + @Test + @DisplayName("should flatten the village lists onto the stored columns") + void saveRoleMapping_shouldFlattenVillageLists() throws Exception { + M_UserServiceRoleMapping2 toSave = mapping(9001, 11); + toSave.setVillageID(new String[] { "501", "502" }); + toSave.setVillageName(new String[] { "Hosur", "Devanahalli" }); + when(employeeMasterRepo.findById(9001)).thenReturn(Optional.empty()); + when(employeeMasterRepo.save(toSave)).thenReturn(toSave); + + service.saveRoleMappingeditedData(toSave, "auth"); + + assertEquals("501,502", toSave.getVillageidDb()); + assertEquals("Hosur,Devanahalli", toSave.getVillageNameDb()); + } + } + + @Nested + @DisplayName("cascadeDeleteAshaMappingsForDeactivation") + class CascadeDeactivationTests { + + @Test + @DisplayName("should retire only this facility when the supervisor still works elsewhere") + void cascade_shouldRetireOnlyThisFacility() { + namedRole("ASHA Supervisor"); + M_UserServiceRoleMapping2 usrRole = mapping(9001, 11); + usrRole.setFacilityID(501); + when(employeeMasterRepo.countByUserIDAndRoleIDAndDeletedFalse(USER_ID, 11)).thenReturn(2L); + + service.cascadeDeleteAshaMappingsForDeactivation(usrRole); + + verify(ashaSupervisorMappingService).cascadeDeleteByUserIDAndFacilityID(USER_ID, 501, "Admin"); + verify(ashaSupervisorMappingService, never()).cascadeDeleteByUserID(anyInt(), anyString()); + } + + @Test + @DisplayName("should retire every mapping when this was the supervisor's last facility") + void cascade_shouldRetireEveryMappingOnLastFacility() { + namedRole("ASHA Supervisor"); + M_UserServiceRoleMapping2 usrRole = mapping(9001, 11); + usrRole.setFacilityID(501); + when(employeeMasterRepo.countByUserIDAndRoleIDAndDeletedFalse(USER_ID, 11)).thenReturn(1L); + + service.cascadeDeleteAshaMappingsForDeactivation(usrRole); + + verify(ashaSupervisorMappingService).cascadeDeleteByUserID(USER_ID, "Admin"); + } + + @Test + @DisplayName("should retire every mapping for a role that is not a supervisor") + void cascade_shouldRetireEveryMappingForNonSupervisor() { + service.cascadeDeleteAshaMappingsForDeactivation(mapping(9001, 11)); + + verify(ashaSupervisorMappingService).cascadeDeleteByUserID(USER_ID, "Admin"); + } + + @Test + @DisplayName("cascadeDeleteAshaMappingsForUser should hand the user straight to the mapping service") + void cascadeForUser_shouldDelegate() { + service.cascadeDeleteAshaMappingsForUser(USER_ID); + + verify(ashaSupervisorMappingService).cascadeDeleteByUserID(USER_ID, "Admin"); + } + } + + @Nested + @DisplayName("Employee lookups") + class LookupTests { + + @Test + @DisplayName("getAllRole should rebuild each role from what the repository holds") + void getAllRole_shouldRebuildRoles() { + M_Role stored = new M_Role(); + stored.setRoleID(11); + stored.setRoleName("Counsellor"); + stored.setProviderServiceMapID(PSM_ID); + when(roleRepo.getAllRole()).thenReturn(new ArrayList<>(List.of(stored))); + + ArrayList roles = service.getAllRole(); + + assertEquals(1, roles.size()); + assertEquals("Counsellor", roles.get(0).getRoleName()); + assertEquals(PSM_ID, roles.get(0).getProviderServiceMapID()); + } + + @Test + @DisplayName("getEmployeeDetails should skip a row the query could not fill") + void getEmployeeDetails_shouldSkipUnfillableRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { 9001, USER_ID, 11, PSM_ID, "a", "b", "c", "d", 1, "e", 2, "f", "g" }); + when(employeeMasterRepo.getEmployeeDetails()).thenReturn(rows); + + assertEquals(1, service.getEmployeeDetails().size()); + } + + @Test + @DisplayName("getAllTitle should rebuild each title from what the repository holds") + void getAllTitle_shouldRebuildTitles() { + M_Title stored = new M_Title(); + stored.setTitleID(1); + stored.setTitleName("Dr"); + when(m_TitleRepo.getAllTitle()).thenReturn(new ArrayList<>(List.of(stored))); + + assertEquals("Dr", service.getAllTitle().get(0).getTitleName()); + } + + @Test + @DisplayName("getAllGender should rebuild each gender from what the repository holds") + void getAllGender_shouldRebuildGenders() { + M_Gender stored = new M_Gender(); + stored.setGenderID(1); + stored.setGenderName("Female"); + when(m_GenderRepo.getAllGender()).thenReturn(new ArrayList<>(List.of(stored))); + + assertEquals("Female", service.getAllGender().get(0).getGenderName()); + } + + @Test + @DisplayName("FindEmployeeName should distinguish a taken user name from a free one") + void findEmployeeName_shouldDistinguishTakenFromFree() { + when(employeeMasterRepoo.findEmployeeByName("asha.rao")).thenReturn(new M_User1()); + when(employeeMasterRepoo.findEmployeeByName("new.user")).thenReturn(null); + + assertEquals("userexist", service.FindEmployeeName("asha.rao")); + assertEquals("usernotexist", service.FindEmployeeName("new.user")); + } + + @Test + @DisplayName("FindEmployeeContact should distinguish a taken number from a free one") + void findEmployeeContact_shouldDistinguishTakenFromFree() { + when(employeeMasterRepoo.findEmployeeByContact("9000000001")).thenReturn(new M_User1()); + when(employeeMasterRepoo.findEmployeeByContact("9000000002")).thenReturn(null); + + assertEquals("contactexist", service.FindEmployeeContact("9000000001")); + assertEquals("contactnotexist", service.FindEmployeeContact("9000000002")); + } + + @Test + @DisplayName("FindEmployeeAadhaar should distinguish a taken number from a free one") + void findEmployeeAadhaar_shouldDistinguishTakenFromFree() { + when(employeeMasterRepoo.findEmployeeAadhaarNo("111122223333")).thenReturn(new M_User1()); + when(employeeMasterRepoo.findEmployeeAadhaarNo("444455556666")).thenReturn(null); + + assertEquals("aadhaarexist", service.FindEmployeeAadhaar("111122223333")); + assertEquals("aadhaarnotexist", service.FindEmployeeAadhaar("444455556666")); + } + + @Test + @DisplayName("FindEmployeeName1 should answer the user record itself") + void findEmployeeName1_shouldAnswerTheRecord() { + M_User1 stored = new M_User1(); + when(employeeMasterRepoo.findEmployeeByName("asha.rao")).thenReturn(stored); + + assertSame(stored, service.FindEmployeeName1("asha.rao")); + } + + @Test + @DisplayName("checkingEmpDetails should report whether the identifiers are already in use") + void checkingEmpDetails_shouldReportWhetherIdentifiersAreTaken() { + when(employeeMasterRepoo.checkingEmpDetails("asha.rao", "1", "2", "3", "4")).thenReturn(new M_User1()); + when(employeeMasterRepoo.checkingEmpDetails("new.user", "1", "2", "3", "4")).thenReturn(null); + + assertTrue(service.checkingEmpDetails("asha.rao", "1", "2", "3", "4")); + assertFalse(service.checkingEmpDetails("new.user", "1", "2", "3", "4")); + } + + @Test + @DisplayName("getQualification should hand back what the repository holds") + void getQualification_shouldHandBackRepositoryContents() { + ArrayList stored = new ArrayList<>(List.of(new M_Userqualification())); + when(m_QualificationRepo.getAllQualification()).thenReturn(stored); + + assertSame(stored, service.getQualification()); + } + + @Test + @DisplayName("getlocationByMapid2 should hand back what the repository holds") + void getlocationByMapid2_shouldHandBackRepositoryContents() { + ArrayList stored = new ArrayList<>(List.of(new Showofficedetails1())); + when(showofficedetails1Repo1.getlocationByMapid(PSM_ID, 301)).thenReturn(stored); + + assertSame(stored, service.getlocationByMapid2(PSM_ID, 301)); + } + + @Test + @DisplayName("getAllByMapId2 should hand back what the repository holds") + void getAllByMapId2_shouldHandBackRepositoryContents() { + ArrayList stored = new ArrayList<>(List.of(new M_ProviderServiceMap1())); + when(m_ProviderServiceMap1Repo.getAllByMapId2(77, 29, 3)).thenReturn(stored); + + assertSame(stored, service.getAllByMapId2(77, 29, 3)); + } + + @Test + @DisplayName("the narrowing searches should each reach their own repository query") + void narrowingSearches_shouldReachTheirOwnQuery() { + ArrayList stored = + new ArrayList<>(List.of(new Showuserdetailsfromuserservicerolemapping())); + when(showuserdetailsfromuserservicerolemappingRepo.EmployeeDetails2(77, 29)).thenReturn(stored); + when(showuserdetailsfromuserservicerolemappingRepo.EmployeeDetails3(77, 11)).thenReturn(stored); + when(showuserdetailsfromuserservicerolemappingRepo.EmployeeDetails4(77, 3)).thenReturn(stored); + when(showuserdetailsfromuserservicerolemappingRepo.EmployeeDetails6(77, USER_ID)).thenReturn(stored); + when(showuserdetailsfromuserservicerolemappingRepo.EmployeeDetails7(77, 29, 301)).thenReturn(stored); + when(showuserdetailsfromuserservicerolemappingRepo.EmployeeDetails8(77, 29, 301, 401)).thenReturn(stored); + when(showuserdetailsfromuserservicerolemappingRepo.EmployeeDetails9(77, 29, 11)).thenReturn(stored); + when(showuserdetailsfromuserservicerolemappingRepo.EmployeeDetails10(77, 29, 11, 3, "asha.rao", USER_ID)) + .thenReturn(stored); + + assertSame(stored, service.getEmployeeDetails2(77, 29)); + assertSame(stored, service.getEmployeeDetails3(77, 11)); + assertSame(stored, service.getEmployeeDetails4(77, 3)); + assertSame(stored, service.getEmployeeDetails6(77, USER_ID)); + assertSame(stored, service.getEmployeeDetails7(77, 29, 301)); + assertSame(stored, service.getEmployeeDetails8(77, 29, 301, 401)); + assertSame(stored, service.getEmployeeDetails9(77, 29, 11)); + assertSame(stored, service.getEmployeeDetails10(77, 29, 11, 3, "asha.rao", USER_ID)); + } + + @Test + @DisplayName("getEmployeeDetails5 should hand back what the view holds") + void getEmployeeDetails5_shouldHandBackViewContents() { + ArrayList stored = new ArrayList<>(List.of(new V_Showuser())); + when(v_ShowuserRepo.EmployeeDetails5()).thenReturn(stored); + + assertSame(stored, service.getEmployeeDetails5()); + } + + @Test + @DisplayName("getcompleteUserDetails should hand back what the view holds") + void getcompleteUserDetails_shouldHandBackViewContents() { + ArrayList stored = new ArrayList<>(List.of(new V_Showuser())); + when(v_ShowuserRepo.getAdminDetails()).thenReturn(stored); + + assertSame(stored, service.getcompleteUserDetails()); + } + + @Test + @DisplayName("getAllReligion and getAllCommunity should hand back what the repositories hold") + void masters_shouldHandBackRepositoryContents() { + ArrayList religions = new ArrayList<>(List.of(new M_Religion())); + ArrayList communities = new ArrayList<>(List.of(new M_Community())); + when(m_ReligionRepo.findAll()).thenReturn(religions); + when(m_CommunityRepo.findAll()).thenReturn(communities); + + assertEquals(1, service.getAllReligion().size()); + assertEquals(1, service.getAllCommunity().size()); + } + } + + @Nested + @DisplayName("getEmployeeDetails4 by provider") + class EmployeeDetails4Tests { + + @Test + @DisplayName("should mark a user locked out after failed sign-ins") + void getEmployeeDetails4_shouldMarkLockedOutUser() { + V_Showuser user = new V_Showuser(); + user.setUserID(USER_ID); + M_User1 record = new M_User1(); + record.setUserID(USER_ID); + record.setFailedAttempt(3); + record.setDeleted(Boolean.TRUE); + record.setLockTimestamp(Timestamp.valueOf("2026-02-17 09:30:00")); + when(v_ShowuserRepo.EmployeeDetails4(77)).thenReturn(new ArrayList<>(List.of(user))); + when(employeeMasterRepoo.findByUserIDIn(anyList())).thenReturn(new ArrayList<>(List.of(record))); + + V_Showuser enriched = service.getEmployeeDetails4(77).get(0); + + assertEquals(3, enriched.getFailedAttempt()); + assertTrue(enriched.getLockedDueToFailedAttempts()); + } + + @Test + @DisplayName("should not mark a user locked out while their account is still active") + void getEmployeeDetails4_shouldNotMarkActiveUserLockedOut() { + V_Showuser user = new V_Showuser(); + user.setUserID(USER_ID); + M_User1 record = new M_User1(); + record.setUserID(USER_ID); + record.setDeleted(Boolean.FALSE); + record.setLockTimestamp(Timestamp.valueOf("2026-02-17 09:30:00")); + when(v_ShowuserRepo.EmployeeDetails4(77)).thenReturn(new ArrayList<>(List.of(user))); + when(employeeMasterRepoo.findByUserIDIn(anyList())).thenReturn(new ArrayList<>(List.of(record))); + + V_Showuser enriched = service.getEmployeeDetails4(77).get(0); + + assertEquals(0, enriched.getFailedAttempt()); + assertFalse(enriched.getLockedDueToFailedAttempts()); + } + + @Test + @DisplayName("should answer an empty result without asking for user records") + void getEmployeeDetails4_shouldAnswerEmptyWithoutFurtherLookups() { + when(v_ShowuserRepo.EmployeeDetails4(77)).thenReturn(new ArrayList<>()); + + assertTrue(service.getEmployeeDetails4(77).isEmpty()); + verify(employeeMasterRepoo, never()).findByUserIDIn(anyList()); + } + } + + @Nested + @DisplayName("Persistence") + class PersistenceTests { + + @Test + @DisplayName("saveEmployee should answer the id of the stored user and encrypt its credentials") + void saveEmployee_shouldStoreAndEncrypt() { + M_User1 toSave = new M_User1(); + M_User1 stored = new M_User1(); + stored.setUserID(USER_ID); + when(employeeMasterRepo11.save(toSave)).thenReturn(stored); + when(encryptUserPassword.encryptUserCredentials(stored)).thenReturn(new OutputResponse()); + + assertEquals(USER_ID, service.saveEmployee(toSave)); + verify(encryptUserPassword).encryptUserCredentials(stored); + } + + @Test + @DisplayName("saveEditData should re-encrypt the credentials it saved") + void saveEditData_shouldReEncryptCredentials() { + M_User1 toSave = new M_User1(); + when(employeeMasterRepo11.save(toSave)).thenReturn(toSave); + + assertSame(toSave, service.saveEditData(toSave)); + verify(encryptUserPassword).encryptUserCredentials(toSave); + } + + @Test + @DisplayName("saveDemography should answer the id of the stored demographics") + void saveDemography_shouldAnswerStoredId() { + M_UserDemographics stored = new M_UserDemographics(); + stored.setDemographicID(5001); + when(m_UserDemographicsRepo.save(any())).thenReturn(stored); + + assertEquals(5001, service.saveDemography(new M_UserDemographics())); + assertEquals(5001, service.saveeditDemo(new M_UserDemographics())); + } + + @Test + @DisplayName("saveeditlangdata should answer the id of the stored language mapping") + void saveeditlangdata_shouldAnswerStoredId() { + M_UserLangMapping stored = new M_UserLangMapping(); + stored.setUserLangID(7001); + when(m_UserLangMappingRepo.save(any())).thenReturn(stored); + + assertEquals(7001, service.saveeditlangdata(new M_UserLangMapping())); + } + + @Test + @DisplayName("mapLanguage and mapRoleUpdation should hand their batches to the repositories") + void batches_shouldReachTheRepositories() { + ArrayList languages = new ArrayList<>(); + ArrayList roles = new ArrayList<>(); + when(m_UserLangMappingRepo.saveAll(anyList())).thenReturn(languages); + when(employeeMasterRepo.saveAll(anyList())).thenReturn(roles); + + assertSame(languages, service.mapLanguage(new ArrayList<>())); + assertSame(roles, service.mapRoleUpdation(new ArrayList<>())); + } + + @Test + @DisplayName("saveeditedData should clear the failed sign-in count for a reinstated user") + void saveeditedData_shouldClearFailedAttemptsOnReinstatement() { + M_User1 toSave = new M_User1(); + toSave.setDeleted(Boolean.FALSE); + toSave.setFailedAttempt(3); + when(employeeMasterRepoo.save(toSave)).thenReturn(toSave); + + service.saveeditedData(toSave); + + assertEquals(0, toSave.getFailedAttempt()); + } + + @Test + @DisplayName("saveeditedData should leave the failed sign-in count alone for a deactivated user") + void saveeditedData_shouldLeaveFailedAttemptsAloneOnDeactivation() { + M_User1 toSave = new M_User1(); + toSave.setDeleted(Boolean.TRUE); + toSave.setFailedAttempt(3); + when(employeeMasterRepoo.save(toSave)).thenReturn(toSave); + + service.saveeditedData(toSave); + + assertEquals(3, toSave.getFailedAttempt()); + } + + @Test + @DisplayName("createProviderAdmin should hash the password before it is stored") + void createProviderAdmin_shouldHashPassword() throws Exception { + M_User1 toCreate = new M_User1(); + toCreate.setPassword("plain-secret"); + ArrayList stored = new ArrayList<>(List.of(toCreate)); + when(employeeMasterRepoo.saveAll(anyList())).thenReturn(stored); + + service.createProviderAdmin(List.of(toCreate)); + + assertTrue(toCreate.getPassword().startsWith("1001:"), + "the stored password must be the salted hash, not the plain text"); + } + + @Test + @DisplayName("createProviderAdmin should refuse an admin with no password") + void createProviderAdmin_shouldRefuseWithoutPassword() { + assertThrows(Exception.class, () -> service.createProviderAdmin(List.of(new M_User1()))); + } + + @Test + @DisplayName("createNewUser should hash the password before it is stored") + void createNewUser_shouldHashPassword() throws Exception { + M_User1 toCreate = new M_User1(); + toCreate.setPassword("plain-secret"); + when(employeeMasterRepoo.saveAll(anyList())).thenReturn(new ArrayList<>(List.of(toCreate))); + + service.createNewUser(List.of(toCreate)); + + assertFalse("plain-secret".equals(toCreate.getPassword())); + } + + @Test + @DisplayName("createNewUser should refuse a user with no password") + void createNewUser_shouldRefuseWithoutPassword() { + assertThrows(Exception.class, () -> service.createNewUser(List.of(new M_User1()))); + } + + @Test + @DisplayName("generateStrongPassword should answer a different hash each time it is called") + void generateStrongPassword_shouldSaltEachHash() throws Exception { + String first = service.generateStrongPassword("plain-secret"); + String second = service.generateStrongPassword("plain-secret"); + + assertNotNull(first); + assertFalse(first.equals(second), "each hash must carry its own salt"); + } + + @Test + @DisplayName("saveBulkUserEmployee should answer the record the repository stored") + void saveBulkUserEmployee_shouldAnswerStoredRecord() { + M_User1 stored = new M_User1(); + stored.setUserID(USER_ID); + when(employeeMasterRepo11.save(any())).thenReturn(stored); + + assertSame(stored, service.saveBulkUserEmployee(new M_User1())); + } + + @Test + @DisplayName("the single-record lookups should each reach their own repository query") + void singleRecordLookups_shouldReachTheirOwnQuery() { + M_User1 user = new M_User1(); + M_UserDemographics demographics = new M_UserDemographics(); + M_UserLangMapping language = new M_UserLangMapping(); + M_UserServiceRoleMapping2 roleMapping = mapping(9001, 11); + when(employeeMasterRepo11.editEmployee(USER_ID)).thenReturn(user); + when(employeeMasterRepoo.findByUserID(USER_ID)).thenReturn(user); + when(m_UserDemographicsRepo.mdedit(USER_ID)).thenReturn(demographics); + when(m_UserDemographicsRepo.findByUserID(USER_ID)).thenReturn(demographics); + when(m_UserDemographicsRepo.save(demographics)).thenReturn(demographics); + when(m_UserLangMappingRepo.ulangmapedit(USER_ID, 1)).thenReturn(language); + when(m_UserLangMappingRepo.findByUserLangID(7001)).thenReturn(language); + when(m_UserLangMappingRepo.save(language)).thenReturn(language); + when(employeeMasterRepo.uRoleMedit(USER_ID, 11)).thenReturn(roleMapping); + when(employeeMasterRepo.uRoledelte(9001)).thenReturn(roleMapping); + when(employeeMasterRepo.findByUSRMappingID(9001)).thenReturn(roleMapping); + when(employeeMasterRepo.save(roleMapping)).thenReturn(roleMapping); + + assertSame(user, service.editEmployee(USER_ID)); + assertSame(user, service.editData(USER_ID)); + assertSame(user, service.getProviderAdminForEdit(USER_ID)); + assertSame(demographics, service.mdedit(USER_ID)); + assertSame(demographics, service.DataByUserID(USER_ID)); + assertSame(demographics, service.saveeditedDemoData(demographics)); + assertSame(language, service.ulangmapedit(USER_ID, 1)); + assertSame(language, service.updateLangMapping(7001)); + assertSame(language, service.saveUserLangEditedData(language)); + assertSame(roleMapping, service.uRoleMedit(USER_ID, 11)); + assertSame(roleMapping, service.uRoledelte(9001)); + assertSame(roleMapping, service.getDataUsrId(9001)); + assertSame(roleMapping, service.saveRoleEdit(roleMapping)); + } + + @Test + @DisplayName("SaveDemographics should hand its batch to the repository") + void saveDemographics_shouldHandBatchToRepository() { + ArrayList stored = new ArrayList<>(); + when(m_UserDemographicsRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.SaveDemographics(new ArrayList<>())); + } + + @Test + @DisplayName("getProviderAdmin should hand back what the repository holds") + void getProviderAdmin_shouldHandBackRepositoryContents() { + ArrayList stored = new ArrayList<>(List.of(new M_User1())); + when(employeeMasterRepoo.getAllProviderAdminData()).thenReturn(stored); + + assertSame(stored, service.getProviderAdmin()); + } + } + + @Nested + @DisplayName("ResetPassword") + class ResetPasswordTests { + + @Test + @DisplayName("should report success when the credential service accepts the new password") + void resetPassword_shouldReportSuccess() { + OutputResponse accepted = new OutputResponse(); + accepted.setResponse("done"); + when(encryptUserPassword.encryptUserCredentials(any())).thenReturn(accepted); + + assertEquals("Password reset successfully", service.ResetPassword(new M_User1())); + } + + @Test + @DisplayName("should report failure when the credential service refuses the new password") + void resetPassword_shouldReportFailure() { + when(encryptUserPassword.encryptUserCredentials(any())).thenReturn(new OutputResponse()); + + assertEquals("Password Not Set Properly", service.ResetPassword(new M_User1())); + } + } + + @Nested + @DisplayName("getMappedRole") + class GetMappedRoleTests { + + @Test + @DisplayName("should split the stored village columns back into lists") + void getMappedRole_shouldSplitVillageColumns() { + V_Userservicerolemapping mapping = new V_Userservicerolemapping(); + mapping.setuSRMappingID(9001); + mapping.setServiceID(3); + mapping.setStateID(29); + mapping.setWorkingDistrictID("301"); + mapping.setVillageidDb("501,502"); + mapping.setVillageNameDb("Hosur,Devanahalli"); + when(v_UserservicerolemappingRepo.getAllRoleOfProvider(77)) + .thenReturn(new ArrayList<>(List.of(mapping))); + when(employeeMasterRepo.getFacilityInfoByMappingIDs(anyList())).thenReturn(new ArrayList<>()); + + V_Userservicerolemapping answered = service.getMappedRole(77).get(0); + + assertArrayEquals(new String[] { "501", "502" }, answered.getVillageID()); + assertArrayEquals(new String[] { "Hosur", "Devanahalli" }, answered.getVillageName()); + } + + @Test + @DisplayName("should clear the block and village details for a mapping with no service line") + void getMappedRole_shouldClearBlockAndVillageWithoutService() { + V_Userservicerolemapping mapping = new V_Userservicerolemapping(); + mapping.setuSRMappingID(9001); + mapping.setStateID(29); + mapping.setWorkingDistrictID("301"); + mapping.setVillageidDb("501"); + when(v_UserservicerolemappingRepo.getAllRoleOfProvider(77)) + .thenReturn(new ArrayList<>(List.of(mapping))); + when(employeeMasterRepo.getFacilityInfoByMappingIDs(anyList())).thenReturn(new ArrayList<>()); + + V_Userservicerolemapping answered = service.getMappedRole(77).get(0); + + assertNull(answered.getVillageID()); + assertNull(answered.getVillageidDb()); + assertNull(answered.getBlockID()); + } + + @Test + @DisplayName("should fill in the state and district the view could not resolve") + void getMappedRole_shouldFillInMissingStateAndDistrict() { + V_Userservicerolemapping mapping = new V_Userservicerolemapping(); + mapping.setuSRMappingID(9001); + mapping.setServiceID(3); + when(v_UserservicerolemappingRepo.getAllRoleOfProvider(77)) + .thenReturn(new ArrayList<>(List.of(mapping))); + when(employeeMasterRepo.getDirectStateDistrictByMappingIDs(anyList())) + .thenReturn(List.of(new Object[] { 9001, 29, "Karnataka", 301, "Bengaluru Urban", 401, "North" })); + when(employeeMasterRepo.getFacilityInfoByMappingIDs(anyList())).thenReturn(new ArrayList<>()); + + V_Userservicerolemapping answered = service.getMappedRole(77).get(0); + + assertEquals(29, answered.getStateID()); + assertEquals("Karnataka", answered.getStateName()); + assertEquals("301", answered.getWorkingDistrictID()); + assertEquals("North", answered.getBlockName()); + } + + @Test + @DisplayName("should attach the facility details the batch lookup resolves") + void getMappedRole_shouldAttachFacilityDetails() { + V_Userservicerolemapping mapping = new V_Userservicerolemapping(); + mapping.setuSRMappingID(9001); + mapping.setServiceID(3); + mapping.setStateID(29); + mapping.setWorkingDistrictID("301"); + when(v_UserservicerolemappingRepo.getAllRoleOfProvider(77)) + .thenReturn(new ArrayList<>(List.of(mapping))); + when(employeeMasterRepo.getFacilityInfoByMappingIDs(anyList())) + .thenReturn(List.of(new Object[] { 9001, 501, "PHC North", 3, "Rural" })); + + V_Userservicerolemapping answered = service.getMappedRole(77).get(0); + + assertEquals(501, answered.getFacilityID()); + assertEquals("PHC North", answered.getFacilityName()); + assertEquals("Rural", answered.getRuralUrban()); + } + + @Test + @DisplayName("should answer an empty list when the view holds nothing") + void getMappedRole_shouldAnswerEmptyListForEmptyView() { + when(v_UserservicerolemappingRepo.getAllRoleOfProvider(77)).thenReturn(null); + + assertTrue(service.getMappedRole(77).isEmpty()); + } + + @Test + @DisplayName("should search by user id when the caller sends no name") + void getMappedRole_shouldSearchByUserIdWithoutAName() { + ArrayList stored = new ArrayList<>(); + when(v_UserservicerolemappingRepo.getDataByUserID(USER_ID)).thenReturn(stored); + + assertSame(stored, service.getMappedRole("", USER_ID)); + } + + @Test + @DisplayName("should search by name when the caller sends no usable user id") + void getMappedRole_shouldSearchByNameWithoutAUserId() { + ArrayList stored = new ArrayList<>(); + when(v_UserservicerolemappingRepo.getDataByName("Asha Rao")).thenReturn(stored); + + assertSame(stored, service.getMappedRole("Asha Rao", 0)); + } + + @Test + @DisplayName("should refuse a search that names both a user and an id") + void getMappedRole_shouldRefuseAmbiguousSearch() { + assertThrows(DataNotFound.class, () -> service.getMappedRole("Asha Rao", USER_ID)); + } + } + + @Nested + @DisplayName("searchMappedLangugeByUserId") + class SearchMappedLanguageTests { + + @Test + @DisplayName("should answer the languages mapped to a real user") + void searchMappedLanguage_shouldAnswerMappedLanguages() { + ArrayList stored = new ArrayList<>(); + when(m_UserLangMappingRepo.getmappedlanguageData(USER_ID)).thenReturn(stored); + + assertSame(stored, service.searchMappedLangugeByUserId(USER_ID)); + } + + @Test + @DisplayName("should refuse a search that names no user") + void searchMappedLanguage_shouldRefuseSearchWithoutUser() { + assertThrows(DataNotFound.class, () -> service.searchMappedLangugeByUserId(0)); + } + } + + @Nested + @DisplayName("getMappedLanguge") + class GetMappedLanguageTests { + + @Test + @DisplayName("should rebuild one mapping per row the query answers") + void getMappedLanguge_shouldRebuildEachRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { 7001, USER_ID, 1, 5, "Kannada", "asha.rao", true, true, true, "n", false, + 5, 5, 5, false }); + when(m_UserLangMappingRepo.getMappedLanguge(77)).thenReturn(rows); + + assertEquals(1, service.getMappedLanguge(77).size()); + } + } + + @Nested + @DisplayName("getEmployeeByDesiganationID") + class EmployeeByDesignationTests { + + @Test + @DisplayName("should mark a user whose signature is on file as active") + void byDesignation_shouldMarkActiveSignature() { + M_User1 user = new M_User1(); + user.setUserID(USER_ID); + EmployeeSignature signature = new EmployeeSignature(); + signature.setDeleted(Boolean.FALSE); + when(employeeMasterRepoo.getempByDesiganation(7, 77)).thenReturn(new ArrayList<>(List.of(user))); + when(employeeSignatureRepo.findOneByUserID(3117L)).thenReturn(signature); + + assertEquals("Active", service.getEmployeeByDesiganationID(7, 77).get(0).getSignatureStatus()); + } + + @Test + @DisplayName("should mark a user whose signature has been retired as inactive") + void byDesignation_shouldMarkRetiredSignature() { + M_User1 user = new M_User1(); + user.setUserID(USER_ID); + EmployeeSignature signature = new EmployeeSignature(); + signature.setDeleted(Boolean.TRUE); + when(employeeMasterRepoo.getempByDesiganation(7, 77)).thenReturn(new ArrayList<>(List.of(user))); + when(employeeSignatureRepo.findOneByUserID(3117L)).thenReturn(signature); + + assertEquals("InActive", service.getEmployeeByDesiganationID(7, 77).get(0).getSignatureStatus()); + } + + @Test + @DisplayName("should leave the signature status unset for a user with none on file") + void byDesignation_shouldLeaveStatusUnsetWithoutSignature() { + M_User1 user = new M_User1(); + user.setUserID(USER_ID); + when(employeeMasterRepoo.getempByDesiganation(7, 77)).thenReturn(new ArrayList<>(List.of(user))); + when(employeeSignatureRepo.findOneByUserID(3117L)).thenReturn(null); + + assertNull(service.getEmployeeByDesiganationID(7, 77).get(0).getSignatureStatus()); + } + } + + @Nested + @DisplayName("getUserRoleTM") + class UserRoleTmTests { + + @Test + @DisplayName("should rebuild one role per row the query answers") + void getUserRoleTM_shouldRebuildEachRow() { + M_UserservicerolemappingForRoleProviderAdmin request = + new M_UserservicerolemappingForRoleProviderAdmin(); + request.setUserID(USER_ID); + request.setProviderServiceMapID(PSM_ID); + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { USER_ID, 11, "TC Specialist", false, 21, "Specialist screen", false }); + when(userservicerolemappingForRoleProviderAdminRepo.getroleofuserTM(USER_ID, PSM_ID)).thenReturn(rows); + + ArrayList roles = service.getUserRoleTM(request); + + assertEquals(1, roles.size()); + assertEquals("TC Specialist", roles.get(0).getRolename()); + } + + @Test + @DisplayName("should answer an empty list when the user holds no telemedicine role") + void getUserRoleTM_shouldAnswerEmptyListWithoutRoles() { + M_UserservicerolemappingForRoleProviderAdmin request = + new M_UserservicerolemappingForRoleProviderAdmin(); + when(userservicerolemappingForRoleProviderAdminRepo.getroleofuserTM(any(), any())) + .thenReturn(new ArrayList<>()); + + assertTrue(service.getUserRoleTM(request).isEmpty()); + } + } + + @Nested + @DisplayName("createAgent") + class CreateAgentTests { + + @Test + @DisplayName("should fill the agent and server placeholders into the configured URL") + void createAgent_shouldFillPlaceholders() { + com.iemr.admin.utils.config.ConfigProperties properties = + new com.iemr.admin.utils.config.ConfigProperties(); + service.setConfigProperties(properties); + + String url = service.createAgent("A-1", "asha.rao"); + + assertNotNull(url); + assertFalse(url.contains("AGENTID"), url); + } + } +} diff --git a/src/test/java/com/iemr/admin/service/employeemaster/EmployeeMasterSupportServicesTest.java b/src/test/java/com/iemr/admin/service/employeemaster/EmployeeMasterSupportServicesTest.java new file mode 100644 index 0000000..230e875 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/employeemaster/EmployeeMasterSupportServicesTest.java @@ -0,0 +1,556 @@ +/* +* 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.admin.service.employeemaster; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +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.admin.data.employeemaster.AshaSupervisorMapping; +import com.iemr.admin.data.employeemaster.EmployeeSignature; +import com.iemr.admin.data.employeemaster.M_Designation; +import com.iemr.admin.data.employeemaster.M_UserServiceRoleMapping2; +import com.iemr.admin.data.employeemaster.USRAgentMapping; +import com.iemr.admin.data.store.M_Facility; +import com.iemr.admin.repo.employeemaster.EmployeeMasterRepo; +import com.iemr.admin.repo.employeemaster.EmployeeSignatureRepo; +import com.iemr.admin.repo.employeemaster.M_DesignationRepo; +import com.iemr.admin.repo.employeemaster.USRAgentMappingRepository; +import com.iemr.admin.repository.store.MainStoreRepo; +import com.iemr.admin.repository.user.AshaSupervisorMappingRepo; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The supporting employee services: the supervisor mapping store, the signature + * store, the CTI agent-id pool and the designation master. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("Employee master support service Test Suite") +class EmployeeMasterSupportServicesTest { + + private static final Integer SUPERVISOR_ID = 3117; + private static final Integer ASHA_ID = 4001; + private static final Integer FACILITY_ID = 501; + + @Mock + private AshaSupervisorMappingRepo ashaSupervisorMappingRepo; + + @Mock + private EmployeeMasterRepo employeeMasterRepo; + + @Mock + private MainStoreRepo mainStoreRepo; + + @InjectMocks + private AshaSupervisorMappingServiceImpl ashaService; + + @Mock + private EmployeeSignatureRepo employeeSignatureRepo; + + @InjectMocks + private EmployeeSignatureServiceImpl signatureService; + + @Mock + private USRAgentMappingRepository usrAgentMappingRepository; + + @Mock + private M_DesignationRepo m_DesignationRepo; + + @InjectMocks + private M_DesignationImpl designationService; + + private static AshaSupervisorMapping mapping(Long id, Integer supervisorId, Integer ashaId) { + AshaSupervisorMapping mapping = new AshaSupervisorMapping(); + mapping.setId(id); + mapping.setSupervisorUserID(supervisorId); + mapping.setAshaUserID(ashaId); + mapping.setFacilityID(FACILITY_ID); + mapping.setCreatedBy("admin"); + return mapping; + } + + private void activeFacility() { + M_Facility facility = new M_Facility(); + facility.setFacilityID(FACILITY_ID); + when(mainStoreRepo.findByFacilityIDAndDeleted(FACILITY_ID, false)).thenReturn(facility); + } + + @Nested + @DisplayName("AshaSupervisorMappingServiceImpl") + class AshaSupervisorMappingTests { + + @Test + @DisplayName("saveAshaSupervisorMappings should refuse a mapping onto a retired facility") + void save_shouldRefuseRetiredFacility() { + when(mainStoreRepo.findByFacilityIDAndDeleted(FACILITY_ID, false)).thenReturn(null); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> ashaService.saveAshaSupervisorMappings(List.of(mapping(null, SUPERVISOR_ID, ASHA_ID)))); + + assertTrue(thrown.getMessage().contains("is no longer active"), thrown.getMessage()); + } + + @Test + @DisplayName("saveAshaSupervisorMappings should reuse a mapping that already exists") + void save_shouldReuseExistingMapping() { + activeFacility(); + AshaSupervisorMapping existing = mapping(1L, SUPERVISOR_ID, ASHA_ID); + when(ashaSupervisorMappingRepo + .findBySupervisorUserIDAndAshaUserIDAndFacilityIDAndDeletedFalse(SUPERVISOR_ID, ASHA_ID, + FACILITY_ID)) + .thenReturn(existing); + + ArrayList saved = + ashaService.saveAshaSupervisorMappings(List.of(mapping(null, SUPERVISOR_ID, ASHA_ID))); + + assertSame(existing, saved.get(0)); + verify(ashaSupervisorMappingRepo, never()).save(any()); + } + + @Test + @DisplayName("saveAshaSupervisorMappings should retire the ASHA's mapping under a different supervisor") + void save_shouldRetireMappingUnderOtherSupervisor() { + activeFacility(); + AshaSupervisorMapping other = mapping(2L, 3118, ASHA_ID); + AshaSupervisorMapping toSave = mapping(null, SUPERVISOR_ID, ASHA_ID); + when(ashaSupervisorMappingRepo + .findByAshaUserIDAndFacilityIDAndDeletedFalseAndSupervisorUserIDNot(ASHA_ID, FACILITY_ID, + SUPERVISOR_ID)) + .thenReturn(new ArrayList<>(List.of(other))); + when(ashaSupervisorMappingRepo.save(toSave)).thenReturn(toSave); + + ashaService.saveAshaSupervisorMappings(List.of(toSave)); + + assertTrue(other.getDeleted(), "an ASHA may report to only one supervisor at a facility"); + assertEquals("admin", other.getModifiedBy()); + verify(ashaSupervisorMappingRepo).save(other); + } + + @Test + @DisplayName("getSupervisorMappingByFacility should hand back what the repository holds") + void getByFacility_shouldHandBackRepositoryContents() { + ArrayList stored = new ArrayList<>(); + when(ashaSupervisorMappingRepo.findActiveMappingsByFacilityID(FACILITY_ID)).thenReturn(stored); + + assertSame(stored, ashaService.getSupervisorMappingByFacility(FACILITY_ID)); + } + + @Test + @DisplayName("getAshasByFacility should hand back what the repository holds") + void getAshas_shouldHandBackRepositoryContents() { + ArrayList stored = new ArrayList<>(); + when(employeeMasterRepo.findAshaUsersByFacilityIDs(anyList())).thenReturn(stored); + + assertSame(stored, ashaService.getAshasByFacility(List.of(FACILITY_ID))); + } + + @Test + @DisplayName("deleteMappings should retire each mapping it can resolve") + void deleteMappings_shouldRetireResolvedMappings() { + AshaSupervisorMapping stored = mapping(1L, SUPERVISOR_ID, ASHA_ID); + when(ashaSupervisorMappingRepo.findById(1L)).thenReturn(Optional.of(stored)); + when(ashaSupervisorMappingRepo.findById(2L)).thenReturn(Optional.empty()); + + ashaService.deleteMappings(List.of(1L, 2L), "admin"); + + assertTrue(stored.getDeleted()); + verify(ashaSupervisorMappingRepo).save(stored); + } + + @Test + @DisplayName("restoreMappings should reinstate each mapping it can resolve") + void restoreMappings_shouldReinstateResolvedMappings() { + AshaSupervisorMapping stored = mapping(1L, SUPERVISOR_ID, ASHA_ID); + stored.setDeleted(Boolean.TRUE); + when(ashaSupervisorMappingRepo.findById(1L)).thenReturn(Optional.of(stored)); + when(ashaSupervisorMappingRepo.findById(2L)).thenReturn(Optional.empty()); + + ashaService.restoreMappings(List.of(1L, 2L), "admin"); + + assertFalse(stored.getDeleted()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("deleteBySupervisorAndFacilities should retire every mapping at the named facilities") + void deleteBySupervisorAndFacilities_shouldRetireEveryMapping() { + AshaSupervisorMapping stored = mapping(1L, SUPERVISOR_ID, ASHA_ID); + when(ashaSupervisorMappingRepo + .findBySupervisorUserIDAndFacilityIDInAndDeletedFalse(SUPERVISOR_ID, List.of(FACILITY_ID))) + .thenReturn(new ArrayList<>(List.of(stored))); + + ashaService.deleteBySupervisorAndFacilities(SUPERVISOR_ID, List.of(FACILITY_ID), "admin"); + + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("cascadeDeleteByUserID should retire the user's mappings on both sides of the relationship") + void cascadeByUser_shouldRetireBothSides() { + AshaSupervisorMapping asSupervisor = mapping(1L, SUPERVISOR_ID, ASHA_ID); + AshaSupervisorMapping asAsha = mapping(2L, 3118, SUPERVISOR_ID); + when(ashaSupervisorMappingRepo.findBySupervisorUserIDAndDeletedFalse(SUPERVISOR_ID)) + .thenReturn(new ArrayList<>(List.of(asSupervisor))); + when(ashaSupervisorMappingRepo.findByAshaUserIDAndDeletedFalse(SUPERVISOR_ID)) + .thenReturn(new ArrayList<>(List.of(asAsha))); + + ashaService.cascadeDeleteByUserID(SUPERVISOR_ID, "admin"); + + assertTrue(asSupervisor.getDeleted()); + assertTrue(asAsha.getDeleted()); + } + + @Test + @DisplayName("cascadeDeleteByFacilityID should retire every mapping at the facility") + void cascadeByFacility_shouldRetireEveryMapping() { + AshaSupervisorMapping stored = mapping(1L, SUPERVISOR_ID, ASHA_ID); + when(ashaSupervisorMappingRepo.findByFacilityIDAndDeletedFalse(FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(stored))); + + ashaService.cascadeDeleteByFacilityID(FACILITY_ID, "admin"); + + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("cascadeDeleteByUserIDAndFacilityID should retire only that user's mappings at that facility") + void cascadeByUserAndFacility_shouldRetireBothSidesAtFacility() { + AshaSupervisorMapping asSupervisor = mapping(1L, SUPERVISOR_ID, ASHA_ID); + AshaSupervisorMapping asAsha = mapping(2L, 3118, SUPERVISOR_ID); + when(ashaSupervisorMappingRepo + .findBySupervisorUserIDAndFacilityIDAndDeletedFalse(SUPERVISOR_ID, FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(asSupervisor))); + when(ashaSupervisorMappingRepo.findByAshaUserIDAndFacilityIDAndDeletedFalse(SUPERVISOR_ID, FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(asAsha))); + + ashaService.cascadeDeleteByUserIDAndFacilityID(SUPERVISOR_ID, FACILITY_ID, "admin"); + + assertTrue(asSupervisor.getDeleted()); + assertTrue(asAsha.getDeleted()); + } + + @Test + @DisplayName("updateAshaMappingsAtomically should retire the old mappings before saving the new ones") + void updateAtomically_shouldRetireThenSave() { + activeFacility(); + AshaSupervisorMapping old = mapping(1L, SUPERVISOR_ID, ASHA_ID); + AshaSupervisorMapping fresh = mapping(null, SUPERVISOR_ID, 4002); + when(ashaSupervisorMappingRepo + .findBySupervisorUserIDAndFacilityIDInAndDeletedFalse(SUPERVISOR_ID, List.of(FACILITY_ID))) + .thenReturn(new ArrayList<>(List.of(old))); + when(ashaSupervisorMappingRepo.save(fresh)).thenReturn(fresh); + + ArrayList saved = ashaService.updateAshaMappingsAtomically( + SUPERVISOR_ID, List.of(FACILITY_ID), List.of(fresh), "admin"); + + assertTrue(old.getDeleted()); + assertSame(fresh, saved.get(0)); + } + + @Test + @DisplayName("updateAshaMappingsAtomically should answer nothing when no new mappings are supplied") + void updateAtomically_shouldAnswerNothingWithoutNewMappings() { + when(ashaSupervisorMappingRepo + .findBySupervisorUserIDAndFacilityIDInAndDeletedFalse(anyInt(), anyList())) + .thenReturn(new ArrayList<>()); + + assertTrue(ashaService + .updateAshaMappingsAtomically(SUPERVISOR_ID, List.of(FACILITY_ID), null, "admin").isEmpty()); + } + } + + @Nested + @DisplayName("EmployeeSignatureServiceImpl") + class SignatureServiceTests { + + @Test + @DisplayName("uploadSignature should overwrite the signature already on file") + void upload_shouldOverwriteExistingSignature() { + EmployeeSignature existing = new EmployeeSignature(); + existing.setUserID(3117L); + existing.setUserSignatureID(9001L); + EmployeeSignature uploaded = new EmployeeSignature(); + uploaded.setUserID(3117L); + uploaded.setFileName("new.png"); + uploaded.setFileType("image/png"); + uploaded.setSignature(new byte[] { 1, 2, 3 }); + uploaded.setCreatedBy("admin"); + when(employeeSignatureRepo.findOneByUserID(3117L)).thenReturn(existing); + when(employeeSignatureRepo.save(existing)).thenReturn(existing); + + assertEquals(9001L, signatureService.uploadSignature(uploaded)); + assertEquals("new.png", existing.getFileName()); + assertEquals("admin", existing.getModifiedBy()); + } + + @Test + @DisplayName("uploadSignature should store a first signature for a user who has none") + void upload_shouldStoreFirstSignature() { + EmployeeSignature uploaded = new EmployeeSignature(); + uploaded.setUserID(3117L); + uploaded.setUserSignatureID(9002L); + when(employeeSignatureRepo.findOneByUserID(3117L)).thenReturn(null); + when(employeeSignatureRepo.save(uploaded)).thenReturn(uploaded); + + assertEquals(9002L, signatureService.uploadSignature(uploaded)); + } + + @Test + @DisplayName("fetchSignature should hand back what the repository holds") + void fetch_shouldHandBackRepositoryContents() { + EmployeeSignature stored = new EmployeeSignature(); + when(employeeSignatureRepo.findOneByUserID(3117L)).thenReturn(stored); + + assertSame(stored, signatureService.fetchSignature(3117L)); + } + + @Test + @DisplayName("existSignature should report whether any signature is on file") + void exist_shouldReportWhetherSignatureIsOnFile() { + when(employeeSignatureRepo.countByUserIDAndSignatureNotNull(3117L)).thenReturn(1L); + when(employeeSignatureRepo.countByUserIDAndSignatureNotNull(3118L)).thenReturn(0L); + + assertTrue(signatureService.existSignature(3117L)); + assertFalse(signatureService.existSignature(3118L)); + } + + @Test + @DisplayName("isSignatureActive should report whether the signature is still in use") + void isActive_shouldReportWhetherSignatureIsInUse() { + when(employeeSignatureRepo.countByUserIDAndSignatureNotNullAndDeletedFalse(3117L)).thenReturn(1L); + when(employeeSignatureRepo.countByUserIDAndSignatureNotNullAndDeletedFalse(3118L)).thenReturn(0L); + + assertTrue(signatureService.isSignatureActive(3117L)); + assertFalse(signatureService.isSignatureActive(3118L)); + } + + @Test + @DisplayName("updateUserSignatureStatus should retire a signature the caller deactivates") + void updateStatus_shouldRetireDeactivatedSignature() { + EmployeeSignature stored = new EmployeeSignature(); + when(employeeSignatureRepo.findOneByUserID(3117L)).thenReturn(stored); + when(employeeSignatureRepo.save(stored)).thenReturn(stored); + + signatureService.updateUserSignatureStatus("{\"userID\":3117,\"active\":false}"); + + assertTrue(stored.getDeleted()); + } + + @Test + @DisplayName("updateUserSignatureStatus should reinstate a signature the caller activates") + void updateStatus_shouldReinstateActivatedSignature() { + EmployeeSignature stored = new EmployeeSignature(); + stored.setDeleted(Boolean.TRUE); + when(employeeSignatureRepo.findOneByUserID(3117L)).thenReturn(stored); + when(employeeSignatureRepo.save(stored)).thenReturn(stored); + + signatureService.updateUserSignatureStatus("{\"userID\":3117,\"active\":true}"); + + assertFalse(stored.getDeleted()); + } + + @Test + @DisplayName("updateUserSignatureStatus should refuse a user who has no signature on file") + void updateStatus_shouldRefuseUserWithoutSignature() { + when(employeeSignatureRepo.findOneByUserID(3117L)).thenReturn(null); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> signatureService.updateUserSignatureStatus("{\"userID\":3117,\"active\":true}")); + + assertTrue(thrown.getMessage().contains("No signature found"), thrown.getMessage()); + } + } + + @Nested + @DisplayName("USRAgentMappingServiceImpl") + class UsrAgentMappingTests { + + private USRAgentMappingServiceImpl agentService() { + USRAgentMappingServiceImpl service = new USRAgentMappingServiceImpl(); + service.setUsrAgentMappingRepository(usrAgentMappingRepository); + return service; + } + + private Set agentRow() { + Set rows = new LinkedHashSet<>(); + rows.add(new Object[] { 1, 9001, null, 4001, null, "A-1", "secret", "104", Boolean.TRUE }); + rows.add(new Object[] { 2 }); + return rows; + } + + @Test + @DisplayName("getAvailableAgentIds should rebuild the free agents and skip an unusable row") + void getAvailableAgentIds_shouldRebuildFreeAgents() throws Exception { + when(usrAgentMappingRepository.getFreeAgentIds("104", 4001)).thenReturn(agentRow()); + + List agents = agentService() + .getAvailableAgentIds("{\"cti_CampaignName\":\"104\",\"providerServiceMapID\":4001}"); + + assertEquals(1, agents.size()); + assertEquals("A-1", agents.get(0).getAgentID()); + } + + @Test + @DisplayName("updateAgentIds should free the previous agent id before claiming the new one") + void updateAgentIds_shouldFreePreviousAgentId() throws Exception { + when(usrAgentMappingRepository.updateUSRMapping(any(), any(), any())).thenReturn(1); + + Integer changed = agentService().updateAgentIds("{\"oldAgentID\":\"A-0\"," + + "\"providerServiceMapID\":4001,\"isAvailable\":false,\"usrMappingID\":9001," + + "\"usrAgentMappingID\":1}"); + + assertEquals(1, changed); + verify(usrAgentMappingRepository).updateUSRMapping(true, null, "A-0", 4001); + } + + @Test + @DisplayName("updateAgentIds should leave the previous agent id alone when none is named") + void updateAgentIds_shouldLeavePreviousAgentIdAlone() throws Exception { + when(usrAgentMappingRepository.updateUSRMapping(any(), any(), any())).thenReturn(1); + + agentService().updateAgentIds("{\"isAvailable\":true,\"usrAgentMappingID\":1}"); + + verify(usrAgentMappingRepository, never()) + .updateUSRMapping(any(Boolean.class), any(), anyString(), anyInt()); + } + + @Test + @DisplayName("createUSRAgentMapping should skip an agent id the provider already holds") + void createUSRAgentMapping_shouldSkipExistingAgent() throws Exception { + when(usrAgentMappingRepository.getExistingAgent(4001, "A-1")).thenReturn(1L); + when(usrAgentMappingRepository.getExistingAgent(4001, "A-2")).thenReturn(0L); + when(usrAgentMappingRepository.save(any())).thenAnswer(call -> call.getArgument(0)); + + List created = agentService().createUSRAgentMapping( + "[{\"agentID\":\"A-1\",\"providerServiceMapID\":4001}," + + "{\"agentID\":\"A-2\",\"providerServiceMapID\":4001}]"); + + assertEquals(1, created.size()); + assertEquals("A-2", created.get(0).getAgentID()); + } + + @Test + @DisplayName("getAvailableCampaigns should hand back what the repository holds") + void getAvailableCampaigns_shouldHandBackRepositoryContents() throws Exception { + when(usrAgentMappingRepository.getAvailableCampaigns(4001)).thenReturn(List.of("104", "1097")); + + assertEquals(2, agentService().getAvailableCampaigns("{\"providerServiceMapID\":4001}").size()); + } + + @Test + @DisplayName("getAllAgentIds should look the agent up directly when the caller names one") + void getAllAgentIds_shouldLookUpNamedAgent() throws Exception { + when(usrAgentMappingRepository + .getUSRAgentMappingByAgentIDAndProviderServiceMapID("A-1", 4001)).thenReturn(agentRow()); + + List agents = agentService() + .getAllAgentIds("{\"agentID\":\"A-1\",\"providerServiceMapID\":4001}"); + + assertEquals(1, agents.size()); + } + + @Test + @DisplayName("getAllAgentIds should filter by availability when the caller asks for it") + void getAllAgentIds_shouldFilterByAvailability() throws Exception { + when(usrAgentMappingRepository.getAllAgentIds(4001, "104", true)).thenReturn(agentRow()); + + assertEquals(1, agentService().getAllAgentIds( + "{\"providerServiceMapID\":4001,\"cti_CampaignName\":\"104\",\"isAvailable\":true}").size()); + } + + @Test + @DisplayName("getAllAgentIds should filter by campaign alone when availability is not named") + void getAllAgentIds_shouldFilterByCampaignAlone() throws Exception { + when(usrAgentMappingRepository.getAllAgentId(4001, "104")).thenReturn(agentRow()); + + assertEquals(1, agentService() + .getAllAgentIds("{\"providerServiceMapID\":4001,\"cti_CampaignName\":\"104\"}").size()); + } + + @Test + @DisplayName("getAllAgentIds should answer every agent under the mapping when nothing is named") + void getAllAgentIds_shouldAnswerEveryAgent() throws Exception { + when(usrAgentMappingRepository.getAllAgentIds(4001)).thenReturn(agentRow()); + + assertEquals(1, agentService().getAllAgentIds("{\"providerServiceMapID\":4001}").size()); + } + + @Test + @DisplayName("updateCTICampaignNameMapping should answer how many mappings moved campaign") + void updateCTICampaignNameMapping_shouldAnswerChangedCount() throws Exception { + when(usrAgentMappingRepository.updateCTICampaignNameMapping("1097", 1)).thenReturn(1); + + assertEquals(1, agentService() + .updateCTICampaignNameMapping("{\"cti_CampaignName\":\"1097\",\"usrAgentMappingID\":1}")); + } + + @Test + @DisplayName("updateDeletedAgentIDStatus should free the agent id of a deleted user") + void updateDeletedAgentIDStatus_shouldFreeAgentId() { + when(usrAgentMappingRepository.updateDeletedAgentIDStatus("A-1")).thenReturn(1); + + agentService().updateDeletedAgentIDStatus("A-1"); + + verify(usrAgentMappingRepository).updateDeletedAgentIDStatus("A-1"); + } + } + + @Nested + @DisplayName("M_DesignationImpl") + class DesignationServiceTests { + + @Test + @DisplayName("getDesinationlist should hand back what the repository holds") + void getDesinationlist_shouldHandBackRepositoryContents() { + ArrayList stored = new ArrayList<>(List.of(new M_Designation())); + when(m_DesignationRepo.getDesinationlist()).thenReturn(stored); + + assertSame(stored, designationService.getDesinationlist()); + } + } +} diff --git a/src/test/java/com/iemr/admin/service/facilitytype/M_facilitytypeServiceImplTest.java b/src/test/java/com/iemr/admin/service/facilitytype/M_facilitytypeServiceImplTest.java new file mode 100644 index 0000000..8551a78 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/facilitytype/M_facilitytypeServiceImplTest.java @@ -0,0 +1,196 @@ +/* +* 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.admin.service.facilitytype; + +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.admin.data.facilitytype.M_facilitytype; +import com.iemr.admin.data.store.M_FacilityLevel; +import com.iemr.admin.repository.facilitytype.M_FacilityLevelRepo; +import com.iemr.admin.repository.facilitytype.M_facilitytypeRepo; + +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; +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.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The facility type service keeps the kinds of health facility a state runs, + * refusing a type name a state already has. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("M_facilitytypeServiceImpl Test Suite") +class M_facilitytypeServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer STATE_ID = 29; + private static final Integer FACILITY_TYPE_ID = 12; + + @Mock + private M_facilitytypeRepo m_facilitytypeRepo; + + @Mock + private M_FacilityLevelRepo m_facilityLevelRepo; + + @InjectMocks + private M_facilitytypeServiceImpl service; + + private static M_facilitytype facilityType() { + M_facilitytype facilityType = new M_facilitytype(); + facilityType.setFacilityTypeID(FACILITY_TYPE_ID); + facilityType.setFacilityTypeName("Primary Health Centre"); + facilityType.setFacilityTypeCode("PHC"); + facilityType.setProviderServiceMapID(PSM_ID); + facilityType.setStateID(STATE_ID); + return facilityType; + } + + @Test + @DisplayName("getAllFicilityData should answer the facility types of the provider asked about") + void getAll_shouldAnswerProvidersFacilityTypes() { + ArrayList held = new ArrayList<>(List.of(facilityType())); + when(m_facilitytypeRepo.getAllFicilityData(PSM_ID)).thenReturn(held); + + assertSame(held, service.getAllFicilityData(PSM_ID)); + } + + @Test + @DisplayName("getFacilityTypesByRuralUrban should narrow the types to the setting asked for") + void getByRuralUrban_shouldNarrowToSetting() { + when(m_facilitytypeRepo.findByProviderServiceMapIDAndRuralUrban(PSM_ID, "Rural")) + .thenReturn(List.of(facilityType())); + + assertEquals(1, service.getFacilityTypesByRuralUrban(PSM_ID, "Rural").size()); + } + + @Test + @DisplayName("addAllFicilityData should store a facility type the state does not have yet") + void add_shouldStoreNewFacilityType() { + ArrayList stored = new ArrayList<>(List.of(facilityType())); + when(m_facilitytypeRepo.existsByFacilityTypeNameAndStateIDAndDeletedFalse(anyString(), anyInt())) + .thenReturn(false); + when(m_facilitytypeRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.addAllFicilityData(List.of(facilityType()))); + } + + @Test + @DisplayName("addAllFicilityData should refuse a facility type name the state already has") + void add_shouldRefuseDuplicateName() { + when(m_facilitytypeRepo.existsByFacilityTypeNameAndStateIDAndDeletedFalse("Primary Health Centre", + STATE_ID)).thenReturn(true); + + RuntimeException refusal = assertThrows(RuntimeException.class, + () -> service.addAllFicilityData(List.of(facilityType()))); + + assertTrue(refusal.getMessage().contains("Primary Health Centre"), refusal.getMessage()); + verify(m_facilitytypeRepo, never()).saveAll(anyList()); + } + + @Test + @DisplayName("editAllFicilityData and updateFacilityData should each reach their own repository query") + void singleRecordOperations_shouldReachTheirOwnQuery() { + M_facilitytype stored = facilityType(); + when(m_facilitytypeRepo.findByFacilityTypeID(FACILITY_TYPE_ID)).thenReturn(stored); + when(m_facilitytypeRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.editAllFicilityData(FACILITY_TYPE_ID)); + assertSame(stored, service.updateFacilityData(stored)); + } + + @Test + @DisplayName("editAllFicilityData should answer nothing when the facility type is unknown") + void edit_shouldAnswerNothingForUnknownFacilityType() { + when(m_facilitytypeRepo.findByFacilityTypeID(-1)).thenReturn(null); + + assertNull(service.editAllFicilityData(-1)); + } + + @Test + @DisplayName("checkFacilityTypeCode should report whether the provider already uses the code") + void checkCode_shouldReportWhetherCodeIsUsed() { + when(m_facilitytypeRepo.findByFacilityTypeCodeAndProviderServiceMapID("PHC", PSM_ID)) + .thenReturn(List.of(facilityType())); + assertTrue(service.checkFacilityTypeCode(facilityType())); + + when(m_facilitytypeRepo.findByFacilityTypeCodeAndProviderServiceMapID("PHC", PSM_ID)) + .thenReturn(new ArrayList<>()); + assertFalse(service.checkFacilityTypeCode(facilityType())); + } + + @Test + @DisplayName("getFacilityLevels should answer the levels still in use, named in order") + void getLevels_shouldAnswerLiveLevels() { + ArrayList levels = new ArrayList<>(List.of(new M_FacilityLevel())); + when(m_facilityLevelRepo.findByDeletedFalseOrderByLevelName()).thenReturn(levels); + + assertSame(levels, service.getFacilityLevels()); + } + + @Test + @DisplayName("getFacilityTypesByBlock should answer the types run in the taluk asked about") + void getByBlock_shouldAnswerTypesOfTaluk() { + when(m_facilitytypeRepo.findFacilityTypesByBlock(3011)).thenReturn(List.of(facilityType())); + + assertEquals(1, service.getFacilityTypesByBlock(3011).size()); + } + + @Test + @DisplayName("getFacilityTypesByState should answer the types run in the state asked about") + void getByState_shouldAnswerTypesOfState() { + when(m_facilitytypeRepo.findByStateID(STATE_ID)).thenReturn(List.of(facilityType())); + + assertEquals(1, service.getFacilityTypesByState(STATE_ID).size()); + } + + @Test + @DisplayName("checkFacilityTypeNameExists should report whether the state already has the name") + void checkName_shouldReportWhetherNameIsUsed() { + when(m_facilitytypeRepo.existsByFacilityTypeNameAndStateIDAndDeletedFalse("Primary Health Centre", + STATE_ID)).thenReturn(true); + assertTrue(service.checkFacilityTypeNameExists("Primary Health Centre", STATE_ID)); + + when(m_facilitytypeRepo.existsByFacilityTypeNameAndStateIDAndDeletedFalse("Sub Centre", STATE_ID)) + .thenReturn(false); + assertFalse(service.checkFacilityTypeNameExists("Sub Centre", STATE_ID)); + } +} diff --git a/src/test/java/com/iemr/admin/service/foetalmonitormaster/FoetalMonitorServiceImplTest.java b/src/test/java/com/iemr/admin/service/foetalmonitormaster/FoetalMonitorServiceImplTest.java new file mode 100644 index 0000000..6731490 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/foetalmonitormaster/FoetalMonitorServiceImplTest.java @@ -0,0 +1,420 @@ +/* +* 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.admin.service.foetalmonitormaster; + +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.admin.data.foetalmonitormaster.FoetalMonitorDeviceID; +import com.iemr.admin.data.foetalmonitormaster.M_FoetalMonitor; +import com.iemr.admin.repo.foetalmonitormaster.FoetalMonitorDeviceIDRepo; +import com.iemr.admin.repo.foetalmonitormaster.FoetalMonitorRepository; +import com.iemr.admin.repository.vanMaster.VanMasterRepository; +import com.iemr.admin.utils.exception.IEMRException; + +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.junit.jupiter.api.Assertions.assertTrue; +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.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * A fetosense device is attached to one van at a time, so the mapping rules here + * decide whether a foetal monitor reading can be traced back to the van it was + * taken in. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("FoetalMonitorServiceImpl Test Suite") +class FoetalMonitorServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer VAN_ID = 71; + private static final Long VFD_ID = 9001L; + + @Mock + private FoetalMonitorRepository foetalMonitorRepository; + + @Mock + private VanMasterRepository masterVanRepo; + + @Mock + private FoetalMonitorDeviceIDRepo foetalMonitorDeviceIDRepo; + + @InjectMocks + private FoetalMonitorServiceImpl service; + + private static M_FoetalMonitor test(Integer id, String name) { + M_FoetalMonitor test = new M_FoetalMonitor(); + test.setFoetalMonitorTestID(id); + test.setTestName(name); + return test; + } + + private static FoetalMonitorDeviceID device() { + FoetalMonitorDeviceID device = new FoetalMonitorDeviceID(); + device.setVfdID(VFD_ID); + device.setDeviceID("FS-1"); + device.setDeviceName("Fetosense 1"); + device.setVanID(VAN_ID); + device.setVanTypeID(1); + device.setParkingPlaceID(31); + device.setVanName("MMU Van 1"); + device.setProviderServiceMapID(PSM_ID); + device.setCreatedBy("admin"); + device.setDeactivated(Boolean.FALSE); + device.setDeleted(Boolean.FALSE); + return device; + } + + @Test + @DisplayName("createFoetalMonitorTestMaster should publish the tests it stored") + void createTestMaster_shouldPublishStoredTests() throws Exception { + when(foetalMonitorRepository.saveAll(anyList())).thenReturn(List.of(test(11, "Non stress test"))); + + String created = service.createFoetalMonitorTestMaster("[{\"testName\":\"Non stress test\"}]"); + + assertTrue(created.contains("Non stress test"), created); + } + + @Test + @DisplayName("createFoetalMonitorTestMaster should answer nothing when it stored fewer than it was given") + void createTestMaster_shouldAnswerNothingOnPartialStore() throws Exception { + when(foetalMonitorRepository.saveAll(anyList())).thenReturn(new ArrayList()); + + assertNull(service.createFoetalMonitorTestMaster("[{\"testName\":\"Non stress test\"}]")); + } + + @Test + @DisplayName("getFoetalMonitorTestMaster should publish the tests of the provider") + void getTestMaster_shouldPublishProviderTests() { + when(foetalMonitorRepository.getByProviderServiceMapID(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(test(11, "Non stress test")))); + + assertTrue(service.getFoetalMonitorTestMaster(PSM_ID).contains("Non stress test")); + } + + @Test + @DisplayName("updateFoetalMonitorTestMaster should publish the test once the edit lands") + void updateTestMaster_shouldPublishEditedTest() { + when(foetalMonitorRepository.updateFoetalMonitorDetails(11, "Non stress test", null, "admin")).thenReturn(1); + when(foetalMonitorRepository.getByFoetalMonitorTestID(11)).thenReturn(test(11, "Non stress test")); + + String published = service.updateFoetalMonitorTestMaster( + "{\"foetalMonitorTestID\":11,\"testName\":\"Non stress test\",\"modifiedBy\":\"admin\"}"); + + assertTrue(published.contains("Non stress test"), published); + } + + @Test + @DisplayName("updateFoetalMonitorTestMaster should answer nothing for a request that names no test") + void updateTestMaster_shouldAnswerNothingWithoutTest() { + assertNull(service.updateFoetalMonitorTestMaster("{\"testName\":\"Non stress test\"}")); + } + + @Test + @DisplayName("updateFoetalMonitorTestMaster should answer nothing when the edit changed nothing") + void updateTestMaster_shouldAnswerNothingWhenNothingChanged() { + when(foetalMonitorRepository.updateFoetalMonitorDetails(anyInt(), any(), any(), any())).thenReturn(0); + + assertNull(service.updateFoetalMonitorTestMaster("{\"foetalMonitorTestID\":11}")); + } + + @Test + @DisplayName("updateFoetalMonitorTestMasterStatus should publish the test once its status has changed") + void updateTestStatus_shouldPublishChangedTest() throws Exception { + when(foetalMonitorRepository.updateFoetalMonitorStatus(11, true)).thenReturn(1); + when(foetalMonitorRepository.getByFoetalMonitorTestID(11)).thenReturn(test(11, "Non stress test")); + + assertTrue(service.updateFoetalMonitorTestMasterStatus(11, true).contains("Non stress test")); + } + + @Test + @DisplayName("updateFoetalMonitorTestMasterStatus should answer nothing when no test changed") + void updateTestStatus_shouldAnswerNothingWhenNothingChanged() throws Exception { + when(foetalMonitorRepository.updateFoetalMonitorStatus(11, true)).thenReturn(0); + + assertNull(service.updateFoetalMonitorTestMasterStatus(11, true)); + } + + @Test + @DisplayName("saveFoetalMonitorDeviceID should report the devices it stored") + void saveDeviceID_shouldReportStoredDevices() throws Exception { + when(foetalMonitorDeviceIDRepo.saveAll(anyList())) + .thenReturn(new ArrayList<>(List.of(device()))); + + assertEquals(1, service.saveFoetalMonitorDeviceID(new ArrayList<>(List.of(device())))); + } + + @Test + @DisplayName("saveFoetalMonitorDeviceID should refuse a run that stored nothing") + void saveDeviceID_shouldRefuseEmptyStore() { + when(foetalMonitorDeviceIDRepo.saveAll(anyList())).thenReturn(new ArrayList()); + + assertThrows(IEMRException.class, + () -> service.saveFoetalMonitorDeviceID(new ArrayList<>(List.of(device())))); + } + + @Test + @DisplayName("getFoetalMonitorDeviceID should publish the devices of the provider") + void getDeviceID_shouldPublishProviderDevices() throws Exception { + when(foetalMonitorDeviceIDRepo.getFoetalMonitorDeviceID(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(device()))); + + String published = service.getFoetalMonitorDeviceID(device()); + + assertTrue(published.contains("fetosenseDeviceIDs"), published); + assertTrue(published.contains("FS-1"), published); + } + + @Test + @DisplayName("getFoetalMonitorDeviceID should report a lookup it could not run") + void getDeviceID_shouldReportFailedLookup() { + when(foetalMonitorDeviceIDRepo.getFoetalMonitorDeviceID(anyInt())) + .thenThrow(new IllegalStateException("no connection")); + + assertThrows(IEMRException.class, () -> service.getFoetalMonitorDeviceID(device())); + } + + @Test + @DisplayName("deleteFoetalMonitorDeviceID should release the van the device was attached to") + void deleteDeviceID_shouldReleaseVan() throws Exception { + FoetalMonitorDeviceID request = device(); + when(foetalMonitorDeviceIDRepo.save(request)).thenReturn(request); + when(masterVanRepo.updateVanFoetalMonitorsmapping(true, VAN_ID)).thenReturn(1); + + assertEquals(1, service.deleteFoetalMonitorDeviceID(request)); + verify(masterVanRepo).updateVanFoetalMonitorsmapping(true, VAN_ID); + } + + @Test + @DisplayName("deleteFoetalMonitorDeviceID should mark the van unmapped when the device is retired") + void deleteDeviceID_shouldMarkVanUnmappedOnRetirement() throws Exception { + FoetalMonitorDeviceID request = device(); + request.setDeleted(Boolean.TRUE); + when(foetalMonitorDeviceIDRepo.save(request)).thenReturn(request); + when(masterVanRepo.updateVanFoetalMonitorsmapping(false, VAN_ID)).thenReturn(1); + + assertEquals(1, service.deleteFoetalMonitorDeviceID(request)); + verify(masterVanRepo).updateVanFoetalMonitorsmapping(false, VAN_ID); + } + + @Test + @DisplayName("deleteFoetalMonitorDeviceID should refuse a device whose van could not be released") + void deleteDeviceID_shouldRefuseWhenVanCannotBeReleased() { + FoetalMonitorDeviceID request = device(); + when(foetalMonitorDeviceIDRepo.save(request)).thenReturn(request); + when(masterVanRepo.updateVanFoetalMonitorsmapping(anyBoolean(), anyInt())).thenReturn(0); + + assertThrows(IEMRException.class, () -> service.deleteFoetalMonitorDeviceID(request)); + } + + @Test + @DisplayName("deleteFoetalMonitorDeviceID should leave the vans alone for a device attached to none") + void deleteDeviceID_shouldLeaveVansAloneForUnattachedDevice() throws Exception { + FoetalMonitorDeviceID request = device(); + request.setVanID(null); + when(foetalMonitorDeviceIDRepo.save(request)).thenReturn(request); + + assertEquals(1, service.deleteFoetalMonitorDeviceID(request)); + verify(masterVanRepo, never()).updateVanFoetalMonitorsmapping(anyBoolean(), anyInt()); + } + + @Test + @DisplayName("getvanIDAndFoetalMonitorDeviceID should publish the vans and devices still free to pair") + void getVanAndDevice_shouldPublishFreePairs() throws Exception { + ArrayList vanRows = new ArrayList<>(); + vanRows.add(new Object[] { VAN_ID, "MMU Van 1", "KA-01-AB-1234" }); + when(masterVanRepo.getVanIDNotMappedWithDevice(1, 31, PSM_ID)).thenReturn(vanRows); + when(foetalMonitorDeviceIDRepo.getFoetalMonitorDeviceIDNotMapped(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(device()))); + + String published = service.getvanIDAndFoetalMonitorDeviceID(device()); + + assertTrue(published.contains("VanIDs"), published); + assertTrue(published.contains("MMU Van 1"), published); + assertTrue(published.contains("deviceIDs"), published); + } + + @Test + @DisplayName("getvanIDAndFoetalMonitorDeviceID should report a lookup it could not run") + void getVanAndDevice_shouldReportFailedLookup() { + when(masterVanRepo.getVanIDNotMappedWithDevice(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertThrows(IEMRException.class, () -> service.getvanIDAndFoetalMonitorDeviceID(device())); + } + + @Test + @DisplayName("vanIDAndDeviceIDMapping should attach the device and mark the van as carrying one") + void mapping_shouldAttachDeviceAndMarkVan() throws Exception { + when(foetalMonitorDeviceIDRepo.createMappingOfVanIDAndDeviceID(anyInt(), anyInt(), anyInt(), anyString(), + anyString(), anyString())).thenReturn(1); + when(masterVanRepo.updateVanFoetalMonitorsmapping(true, VAN_ID)).thenReturn(1); + + assertEquals(1, service.vanIDAndDeviceIDMapping(device())); + } + + @Test + @DisplayName("vanIDAndDeviceIDMapping should refuse a pairing the van could not be marked for") + void mapping_shouldRefuseWhenVanCannotBeMarked() { + when(foetalMonitorDeviceIDRepo.createMappingOfVanIDAndDeviceID(anyInt(), anyInt(), anyInt(), anyString(), + anyString(), anyString())).thenReturn(1); + when(masterVanRepo.updateVanFoetalMonitorsmapping(anyBoolean(), anyInt())).thenReturn(0); + + assertThrows(IEMRException.class, () -> service.vanIDAndDeviceIDMapping(device())); + } + + @Test + @DisplayName("vanIDAndDeviceIDMapping should refuse a pairing the device could not take") + void mapping_shouldRefuseWhenDeviceCannotTakePairing() { + when(foetalMonitorDeviceIDRepo.createMappingOfVanIDAndDeviceID(anyInt(), anyInt(), anyInt(), anyString(), + anyString(), anyString())).thenReturn(0); + + assertThrows(IEMRException.class, () -> service.vanIDAndDeviceIDMapping(device())); + } + + @Test + @DisplayName("updateFoetalMonitorDeviceID should report the device it saved") + void updateDeviceID_shouldReportSavedDevice() throws Exception { + FoetalMonitorDeviceID request = device(); + when(foetalMonitorDeviceIDRepo.save(request)).thenReturn(request); + + assertEquals(1, service.updateFoetalMonitorDeviceID(request)); + } + + @Test + @DisplayName("updateFoetalMonitorDeviceID should refuse an edit the store did not take") + void updateDeviceID_shouldRefuseUntakenEdit() { + when(foetalMonitorDeviceIDRepo.save(any())).thenReturn(null); + + assertThrows(IEMRException.class, () -> service.updateFoetalMonitorDeviceID(device())); + } + + @Test + @DisplayName("getVanIDMappingWorklist should publish the pairings on record") + void getWorklist_shouldPublishPairings() throws Exception { + when(foetalMonitorDeviceIDRepo.getMappedWorklist(1, 31, PSM_ID)) + .thenReturn(new ArrayList<>(List.of(device()))); + + assertTrue(service.getVanIDMappingWorklist(device()).contains("FS-1")); + } + + @Test + @DisplayName("getVanIDMappingWorklist should report a lookup it could not run") + void getWorklist_shouldReportFailedLookup() { + when(foetalMonitorDeviceIDRepo.getMappedWorklist(any(), any(), any())) + .thenThrow(new IllegalStateException("no connection")); + + assertThrows(IEMRException.class, () -> service.getVanIDMappingWorklist(device())); + } + + @Test + @DisplayName("updatingvanIDAndDeviceIDMapping should clear the old van before attaching the new one") + void updateMapping_shouldClearOldVanFirst() throws Exception { + when(foetalMonitorDeviceIDRepo.updateVanDetailsToNull(VFD_ID)).thenReturn(1); + when(foetalMonitorDeviceIDRepo.createMappingOfVanIDAndDeviceID(anyInt(), anyInt(), anyInt(), anyString(), + any(), anyString())).thenReturn(1); + + assertEquals(1, service.updatingvanIDAndDeviceIDMapping(device())); + verify(foetalMonitorDeviceIDRepo).updateVanDetailsToNull(VFD_ID); + } + + @Test + @DisplayName("updatingvanIDAndDeviceIDMapping should refuse an edit whose old van could not be cleared") + void updateMapping_shouldRefuseWhenOldVanCannotBeCleared() { + when(foetalMonitorDeviceIDRepo.updateVanDetailsToNull(VFD_ID)).thenReturn(0); + + assertThrows(IEMRException.class, () -> service.updatingvanIDAndDeviceIDMapping(device())); + } + + @Test + @DisplayName("updatingvanIDAndDeviceIDMapping should refuse an edit the new van could not take") + void updateMapping_shouldRefuseWhenNewVanCannotTake() { + when(foetalMonitorDeviceIDRepo.updateVanDetailsToNull(VFD_ID)).thenReturn(1); + when(foetalMonitorDeviceIDRepo.createMappingOfVanIDAndDeviceID(anyInt(), anyInt(), anyInt(), anyString(), + any(), anyString())).thenReturn(0); + + assertThrows(IEMRException.class, () -> service.updatingvanIDAndDeviceIDMapping(device())); + } + + @Test + @DisplayName("deleteVanIDAndDeviceIDMapping should release the pairing and free the van") + void deleteMapping_shouldReleasePairingAndFreeVan() throws Exception { + FoetalMonitorDeviceID request = device(); + request.setDeactivated(Boolean.TRUE); + when(foetalMonitorDeviceIDRepo.deleteMapping(true, VFD_ID)).thenReturn(1); + when(masterVanRepo.updateVanFoetalMonitorsmapping(false, VAN_ID)).thenReturn(1); + + assertEquals(1, service.deleteVanIDAndDeviceIDMapping(request)); + } + + @Test + @DisplayName("deleteVanIDAndDeviceIDMapping should refuse to reinstate a van another device already holds") + void deleteMapping_shouldRefuseVanHeldByAnotherDevice() { + FoetalMonitorDeviceID request = device(); + request.setDeactivated(Boolean.FALSE); + when(foetalMonitorDeviceIDRepo.getMappedVanDetails(VAN_ID)).thenReturn(1); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> service.deleteVanIDAndDeviceIDMapping(request)); + assertTrue(thrown.getMessage().contains("already mapped with a device"), thrown.getMessage()); + } + + @Test + @DisplayName("deleteVanIDAndDeviceIDMapping should reinstate a pairing for a van that is free") + void deleteMapping_shouldReinstatePairingForFreeVan() throws Exception { + FoetalMonitorDeviceID request = device(); + request.setDeactivated(Boolean.FALSE); + when(foetalMonitorDeviceIDRepo.getMappedVanDetails(VAN_ID)).thenReturn(0); + when(foetalMonitorDeviceIDRepo.deleteMapping(false, VFD_ID)).thenReturn(1); + when(masterVanRepo.updateVanFoetalMonitorsmapping(true, VAN_ID)).thenReturn(1); + + assertEquals(1, service.deleteVanIDAndDeviceIDMapping(request)); + } + + @Test + @DisplayName("deleteVanIDAndDeviceIDMapping should refuse a pairing the store would not change") + void deleteMapping_shouldRefuseUnchangedPairing() { + FoetalMonitorDeviceID request = device(); + request.setDeactivated(Boolean.TRUE); + when(foetalMonitorDeviceIDRepo.deleteMapping(anyBoolean(), any())).thenReturn(0); + + assertThrows(IEMRException.class, () -> service.deleteVanIDAndDeviceIDMapping(request)); + } +} diff --git a/src/test/java/com/iemr/admin/service/health/HealthServiceTest.java b/src/test/java/com/iemr/admin/service/health/HealthServiceTest.java new file mode 100644 index 0000000..b2e8176 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/health/HealthServiceTest.java @@ -0,0 +1,472 @@ +/* +* 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.admin.service.health; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import javax.sql.DataSource; + +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.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +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.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * The health endpoint is what the deployment's monitoring watches, so it has to + * tell a database that is merely slow apart from one that is unreachable. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("HealthService Test Suite") +class HealthServiceTest { + + @Mock + private DataSource dataSource; + + @Mock + private RedisConnectionFactory redisConnectionFactory; + + @Mock + private Connection connection; + + @Mock + private Statement statement; + + @Mock + private RedisConnection redisConnection; + + @SuppressWarnings("unchecked") + private static ObjectProvider providerOf(T value) { + ObjectProvider provider = mock(ObjectProvider.class); + when(provider.getIfAvailable()).thenReturn(value); + return provider; + } + + /** + * Builds the service without a data source so its constructor starts no background + * cycle, then attaches the stores under test. That keeps the diagnostics the tests + * drive from racing a scheduled cycle over the same mocks. + */ + private HealthService serviceWith(DataSource ds, RedisConnectionFactory redis) { + HealthService service = new HealthService(providerOf(null), providerOf(redis)); + ReflectionTestUtils.setField(service, "dataSource", ds); + ((AtomicLong) ReflectionTestUtils.getField(service, "lastDiagnosticRunAt")).set(0); + return service; + } + + private void databaseAnswers() throws SQLException { + when(dataSource.getConnection()).thenReturn(connection); + when(connection.createStatement()).thenReturn(statement); + } + + private void redisAnswers() { + when(redisConnectionFactory.getConnection()).thenReturn(redisConnection); + } + + private static ResultSet countingResultSet(int count) throws SQLException { + ResultSet rs = mock(ResultSet.class); + when(rs.next()).thenReturn(true); + when(rs.getInt("cnt")).thenReturn(count); + return rs; + } + + private static ResultSet statusResultSet(long value) throws SQLException { + ResultSet rs = mock(ResultSet.class); + when(rs.next()).thenReturn(true); + when(rs.getLong("Value")).thenReturn(value); + when(rs.getInt("Value")).thenReturn((int) value); + return rs; + } + + @SuppressWarnings("unchecked") + private static String statusOf(Map health, String service) { + return (String) ((Map) health.get(service)).get("status"); + } + + @SuppressWarnings("unchecked") + private static String severityOf(Map health, String service) { + return (String) ((Map) health.get(service)).get("severity"); + } + + @Nested + @DisplayName("checkHealth") + class CheckHealthTests { + + @Test + @DisplayName("should report the deployment up when both stores answer") + void checkHealth_shouldReportUpWhenBothStoresAnswer() throws Exception { + databaseAnswers(); + redisAnswers(); + HealthService service = serviceWith(dataSource, redisConnectionFactory); + + Map health = service.checkHealth(); + + assertEquals("UP", health.get("status")); + assertEquals("UP", statusOf(health, "mysql")); + assertEquals("UP", statusOf(health, "redis")); + assertNotNull(health.get("checkedAt")); + } + + @Test + @DisplayName("should report the deployment down when the database cannot be reached") + void checkHealth_shouldReportDownWhenDatabaseUnreachable() throws Exception { + when(dataSource.getConnection()).thenThrow(new SQLException("connection refused")); + redisAnswers(); + HealthService service = serviceWith(dataSource, redisConnectionFactory); + + Map health = service.checkHealth(); + + assertEquals("DOWN", health.get("status")); + assertEquals("DOWN", statusOf(health, "mysql")); + assertEquals("CRITICAL", severityOf(health, "mysql")); + } + + @Test + @DisplayName("should report the deployment down when Redis cannot be reached") + void checkHealth_shouldReportDownWhenRedisUnreachable() throws Exception { + databaseAnswers(); + when(redisConnectionFactory.getConnection()) + .thenThrow(new IllegalStateException("connection refused")); + HealthService service = serviceWith(dataSource, redisConnectionFactory); + + Map health = service.checkHealth(); + + assertEquals("DOWN", health.get("status")); + assertEquals("DOWN", statusOf(health, "redis")); + } + + @Test + @DisplayName("should report a store that is not configured rather than call it down") + void checkHealth_shouldReportUnconfiguredStore() { + HealthService service = serviceWith(null, null); + + Map health = service.checkHealth(); + + assertEquals("UP", health.get("status"), + "a deployment without these stores is not itself unhealthy"); + assertEquals("NOT_CONFIGURED", statusOf(health, "mysql")); + assertEquals("INFO", severityOf(health, "mysql")); + assertEquals("NOT_CONFIGURED", statusOf(health, "redis")); + } + + @Test + @DisplayName("should report a degraded database as still up but flagged") + void checkHealth_shouldReportDegradedDatabase() throws Exception { + databaseAnswers(); + redisAnswers(); + HealthService service = serviceWith(dataSource, redisConnectionFactory); + ReflectionTestUtils.invokeMethod( + ReflectionTestUtils.getField(service, "cachedDbSeverity"), "set", "WARNING"); + + Map health = service.checkHealth(); + + assertEquals("UP", health.get("status"), "a degraded database is still serving requests"); + assertEquals("DEGRADED", statusOf(health, "mysql")); + } + + @Test + @DisplayName("should report a critically degraded database as down") + void checkHealth_shouldReportCriticalDatabaseAsDown() throws Exception { + databaseAnswers(); + redisAnswers(); + HealthService service = serviceWith(dataSource, redisConnectionFactory); + ReflectionTestUtils.invokeMethod( + ReflectionTestUtils.getField(service, "cachedDbSeverity"), "set", "CRITICAL"); + + assertEquals("DOWN", service.checkHealth().get("status")); + } + } + + @Nested + @DisplayName("Background diagnostics") + class DiagnosticTests { + + private HealthService diagnosingService() throws SQLException { + databaseAnswers(); + return serviceWith(dataSource, redisConnectionFactory); + } + + private String severityAfterDiagnostics(HealthService service) { + ((AtomicLong) ReflectionTestUtils.getField(service, "lastDiagnosticRunAt")).set(0); + ReflectionTestUtils.invokeMethod(service, "runAdvancedMySQLDiagnostics"); + return (String) ReflectionTestUtils.invokeMethod( + ReflectionTestUtils.getField(service, "cachedDbSeverity"), "get"); + } + + private void everyCheckClean() throws SQLException { + when(statement.executeQuery(anyString())).thenAnswer(call -> { + String sql = call.getArgument(0); + if (sql.contains("max_connections")) { + return statusResultSet(500); + } + if (sql.startsWith("SHOW")) { + return statusResultSet(0); + } + return countingResultSet(0); + }); + } + + @Test + @DisplayName("should report a healthy database when every check is clean") + void diagnostics_shouldReportHealthyDatabase() throws Exception { + HealthService service = diagnosingService(); + everyCheckClean(); + + assertEquals("OK", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should warn when more processes are stuck than the threshold allows") + void diagnostics_shouldWarnOnStuckProcesses() throws Exception { + HealthService service = diagnosingService(); + when(statement.executeQuery(anyString())).thenAnswer(call -> { + String sql = call.getArgument(0); + if (sql.contains("PROCESSLIST")) { + return countingResultSet(10); + } + if (sql.contains("max_connections")) { + return statusResultSet(500); + } + if (sql.startsWith("SHOW")) { + return statusResultSet(0); + } + return countingResultSet(0); + }); + + assertEquals("WARNING", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should stay healthy when a few processes are stuck but under the threshold") + void diagnostics_shouldStayHealthyUnderStuckThreshold() throws Exception { + HealthService service = diagnosingService(); + when(statement.executeQuery(anyString())).thenAnswer(call -> { + String sql = call.getArgument(0); + if (sql.contains("PROCESSLIST")) { + return countingResultSet(2); + } + if (sql.contains("max_connections")) { + return statusResultSet(500); + } + if (sql.startsWith("SHOW")) { + return statusResultSet(0); + } + return countingResultSet(0); + }); + + assertEquals("OK", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should escalate to critical when several transactions run long") + void diagnostics_shouldEscalateOnManyLongTransactions() throws Exception { + HealthService service = diagnosingService(); + when(statement.executeQuery(anyString())).thenAnswer(call -> { + String sql = call.getArgument(0); + if (sql.contains("INNODB_TRX")) { + return countingResultSet(6); + } + if (sql.contains("max_connections")) { + return statusResultSet(500); + } + if (sql.startsWith("SHOW")) { + return statusResultSet(0); + } + return countingResultSet(0); + }); + + assertEquals("CRITICAL", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should warn when a single transaction runs long") + void diagnostics_shouldWarnOnOneLongTransaction() throws Exception { + HealthService service = diagnosingService(); + when(statement.executeQuery(anyString())).thenAnswer(call -> { + String sql = call.getArgument(0); + if (sql.contains("INNODB_TRX")) { + return countingResultSet(2); + } + if (sql.contains("max_connections")) { + return statusResultSet(500); + } + if (sql.startsWith("SHOW")) { + return statusResultSet(0); + } + return countingResultSet(0); + }); + + assertEquals("WARNING", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should warn when new deadlocks have happened since the previous cycle") + void diagnostics_shouldWarnOnNewDeadlocks() throws Exception { + HealthService service = diagnosingService(); + when(statement.executeQuery(anyString())).thenAnswer(call -> { + String sql = call.getArgument(0); + if (sql.contains("Innodb_deadlocks")) { + return statusResultSet(3); + } + if (sql.contains("max_connections")) { + return statusResultSet(500); + } + if (sql.startsWith("SHOW")) { + return statusResultSet(0); + } + return countingResultSet(0); + }); + + assertEquals("WARNING", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should warn when new slow queries have been logged since the previous cycle") + void diagnostics_shouldWarnOnNewSlowQueries() throws Exception { + HealthService service = diagnosingService(); + when(statement.executeQuery(anyString())).thenAnswer(call -> { + String sql = call.getArgument(0); + if (sql.contains("Slow_queries")) { + return statusResultSet(7); + } + if (sql.contains("max_connections")) { + return statusResultSet(500); + } + if (sql.startsWith("SHOW")) { + return statusResultSet(0); + } + return countingResultSet(0); + }); + + assertEquals("WARNING", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should escalate to critical when the connection pool is nearly exhausted") + void diagnostics_shouldEscalateOnExhaustedPool() throws Exception { + HealthService service = diagnosingService(); + when(statement.executeQuery(anyString())).thenAnswer(call -> { + String sql = call.getArgument(0); + if (sql.contains("Threads_connected")) { + return statusResultSet(490); + } + if (sql.contains("max_connections")) { + return statusResultSet(500); + } + if (sql.startsWith("SHOW")) { + return statusResultSet(0); + } + return countingResultSet(0); + }); + + assertEquals("CRITICAL", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should warn when connection usage is high but not yet exhausted") + void diagnostics_shouldWarnOnHighConnectionUsage() throws Exception { + HealthService service = diagnosingService(); + when(statement.executeQuery(anyString())).thenAnswer(call -> { + String sql = call.getArgument(0); + if (sql.contains("Threads_connected")) { + return statusResultSet(450); + } + if (sql.contains("max_connections")) { + return statusResultSet(500); + } + if (sql.startsWith("SHOW")) { + return statusResultSet(0); + } + return countingResultSet(0); + }); + + assertEquals("WARNING", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should report critical when it cannot open a connection to diagnose at all") + void diagnostics_shouldReportCriticalWhenConnectionCannotBeOpened() throws Exception { + HealthService service = diagnosingService(); + when(dataSource.getConnection()).thenThrow(new SQLException("connection refused")); + + assertEquals("CRITICAL", severityAfterDiagnostics(service)); + } + + @Test + @DisplayName("should keep the previous verdict when a cycle is asked for too soon") + void diagnostics_shouldKeepPreviousVerdictWhenAskedTooSoon() throws Exception { + HealthService service = diagnosingService(); + everyCheckClean(); + severityAfterDiagnostics(service); + ReflectionTestUtils.invokeMethod( + ReflectionTestUtils.getField(service, "cachedDbSeverity"), "set", "WARNING"); + ReflectionTestUtils.invokeMethod(service, "runAdvancedMySQLDiagnostics"); + + assertEquals("WARNING", + ReflectionTestUtils.invokeMethod( + ReflectionTestUtils.getField(service, "cachedDbSeverity"), "get"), + "a cycle inside the guard window must not overwrite the standing verdict"); + } + + @Test + @DisplayName("should survive a check whose query the database refuses") + void diagnostics_shouldSurviveRefusedQuery() throws Exception { + HealthService service = diagnosingService(); + when(statement.executeQuery(anyString())).thenThrow(new SQLException("access denied")); + + assertEquals("OK", severityAfterDiagnostics(service), + "a check that cannot run must not by itself condemn the database"); + } + } + + @Test + @DisplayName("shutdownDiagnostics should stop the background cycle") + void shutdownDiagnostics_shouldStopBackgroundCycle() throws Exception { + databaseAnswers(); + HealthService service = new HealthService(providerOf(dataSource), providerOf(redisConnectionFactory)); + + service.shutdownDiagnostics(); + + java.util.concurrent.ExecutorService scheduler = (java.util.concurrent.ExecutorService) + ReflectionTestUtils.getField(service, "diagnosticScheduler"); + assertEquals(true, scheduler.isShutdown()); + } +} diff --git a/src/test/java/com/iemr/admin/service/item/ItemServiceImplTest.java b/src/test/java/com/iemr/admin/service/item/ItemServiceImplTest.java new file mode 100644 index 0000000..4fa9c95 --- /dev/null +++ b/src/test/java/com/iemr/admin/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.admin.service.item; + +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.admin.data.items.CodeChecker; +import com.iemr.admin.data.items.ItemMaster; +import com.iemr.admin.data.items.M_ItemCategory; +import com.iemr.admin.data.items.M_ItemForm; +import com.iemr.admin.data.items.M_Route; +import com.iemr.admin.repository.item.ItemCategoryRepo; +import com.iemr.admin.repository.item.ItemFormRepo; +import com.iemr.admin.repository.item.ItemRepo; +import com.iemr.admin.repository.item.RouteRepo; +import com.iemr.admin.repository.itemfacilitymapping.M_itemfacilitymappingRepo; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The item service maintains the inventory catalogue and the codes that keep + * each entry unique within a provider. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ItemServiceImpl Test Suite") +class ItemServiceImplTest { + + private static final Integer PSM_ID = 4001; + + @Mock + private ItemRepo itemRepo; + + @Mock + private ItemCategoryRepo itemCategoryRepo; + + @Mock + private RouteRepo routeRepo; + + @Mock + private ItemFormRepo itemFormRepo; + + @Mock + private M_itemfacilitymappingRepo itemfacilitymappingRepo; + + @InjectMocks + private ItemServiceImpl service; + + private static M_ItemCategory category(Integer id) { + M_ItemCategory category = new M_ItemCategory(); + category.setItemCategoryID(id); + return category; + } + + @Test + @DisplayName("getItemCategory should read the whole catalogue when retired categories are wanted too") + void getItemCategory_shouldReadWholeCatalogue() { + List stored = List.of(category(31)); + when(itemCategoryRepo.findByProviderServiceMapIDOrderByItemCategoryName(PSM_ID)).thenReturn(stored); + + assertSame(stored, service.getItemCategory(true, PSM_ID)); + } + + @Test + @DisplayName("getItemCategory should read only the live categories when retired ones are excluded") + void getItemCategory_shouldReadLiveCategories() { + List stored = List.of(category(31)); + when(itemCategoryRepo.findByDeletedAndProviderServiceMapIDOrderByItemCategoryName(false, PSM_ID)) + .thenReturn(stored); + + assertSame(stored, service.getItemCategory(false, PSM_ID)); + } + + @Test + @DisplayName("getItemCategory should answer nothing when the caller names no provider") + void getItemCategory_shouldAnswerNothingWithoutProvider() { + assertTrue(service.getItemCategory(true, null).isEmpty()); + verify(itemCategoryRepo, never()).findByProviderServiceMapIDOrderByItemCategoryName(anyInt()); + } + + @Test + @DisplayName("the item lookups should each reach their own repository query") + void itemLookups_shouldReachTheirOwnQuery() { + ItemMaster item = new ItemMaster(); + List items = List.of(item); + when(itemRepo.save(item)).thenReturn(item); + when(itemRepo.saveAll(anyList())).thenReturn(items); + when(itemRepo.findByProviderServiceMapIDOrderByItemName(PSM_ID)).thenReturn(items); + when(itemRepo.findByItemID(101)).thenReturn(item); + when(itemRepo.findDetailOne(101)).thenReturn(item); + when(itemRepo.getItemMasters(PSM_ID, 31)).thenReturn(items); + when(itemRepo.deleteItemMaster(101, true)).thenReturn(1); + when(itemRepo.discontinueItemMaster(101, true)).thenReturn(1); + when(itemCategoryRepo.findByItemCategoryID(31)).thenReturn(category(31)); + when(routeRepo.getAll()).thenReturn(List.of(new M_Route())); + when(itemFormRepo.getAll()).thenReturn(List.of(new M_ItemForm())); + + assertSame(item, service.createItemMaster(item)); + assertSame(items, service.addAllItemMaster(new ArrayList<>())); + assertSame(items, service.getItemMaster(PSM_ID)); + assertSame(item, service.getItemMasterByID(101)); + assertSame(item, service.getItemMasterCatByID(101)); + assertSame(items, service.getItemMasters(PSM_ID, 31)); + assertEquals(1, service.blockItemMaster(101, true)); + assertEquals(1, service.discontinueItemMaster(101, true)); + assertEquals(31, service.getItemCategory(31).getItemCategoryID()); + assertEquals(1, service.getItemRouteProviderServiceMapID(PSM_ID).size()); + assertEquals(1, service.getItemFormProviderServiceMapID(PSM_ID).size()); + } + + @Test + @DisplayName("updateItemIssueConfig should count only the categories that name an issue type") + void updateItemIssueConfig_shouldCountOnlyComplete() { + M_ItemCategory complete = category(31); + complete.setIssueType("FIFO"); + M_ItemCategory incomplete = category(32); + when(itemCategoryRepo.updateIssueConfig(31, "FIFO")).thenReturn(1); + + assertEquals(1, service.updateItemIssueConfig(List.of(complete, incomplete))); + verify(itemCategoryRepo, never()).updateIssueConfig(32, null); + } + + @Test + @DisplayName("updateExpiryAlert should count only the categories that name an alert window") + void updateExpiryAlert_shouldCountOnlyComplete() { + M_ItemCategory complete = category(31); + complete.setAlertBeforeDays(30); + M_ItemCategory incomplete = category(32); + when(itemCategoryRepo.updateExpiryAlert(31, 30)).thenReturn(1); + + assertEquals(1, service.updateExpiryAlert(List.of(complete, incomplete))); + } + + @Test + @DisplayName("createItemCategories should answer the id of the first category it stored") + void createItemCategories_shouldAnswerFirstStoredId() { + when(itemCategoryRepo.saveAll(anyList())).thenReturn(List.of(category(31))); + + assertEquals(31, service.createItemCategories(new ArrayList<>())); + } + + @Test + @DisplayName("createItemCategories should answer zero when nothing was stored") + void createItemCategories_shouldAnswerZeroWhenNothingStored() { + when(itemCategoryRepo.saveAll(anyList())).thenReturn(new ArrayList<>()); + + assertEquals(0, service.createItemCategories(new ArrayList<>())); + } + + @Test + @DisplayName("createItemForms should answer the id of the first form it stored") + void createItemForms_shouldAnswerFirstStoredId() { + M_ItemForm form = new M_ItemForm(); + form.setItemFormID(11); + when(itemFormRepo.saveAll(anyList())).thenReturn(List.of(form)); + + assertEquals(11, service.createItemForms(new ArrayList<>())); + when(itemFormRepo.saveAll(anyList())).thenReturn(new ArrayList<>()); + assertEquals(0, service.createItemForms(new ArrayList<>())); + } + + @Test + @DisplayName("createRoutes should answer the id of the first route it stored") + void createRoutes_shouldAnswerFirstStoredId() { + M_Route route = new M_Route(); + route.setRouteID(21); + when(routeRepo.saveAll(anyList())).thenReturn(List.of(route)); + + assertEquals(21, service.createRoutes(new ArrayList<>())); + when(routeRepo.saveAll(anyList())).thenReturn(new ArrayList<>()); + assertEquals(0, service.createRoutes(new ArrayList<>())); + } + + @Test + @DisplayName("the edit and block calls should each reach their own repository query") + void editAndBlockCalls_shouldReachTheirOwnQuery() { + M_ItemCategory cat = category(31); + cat.setItemCategoryDesc("Drugs"); + cat.setModifiedBy("admin"); + cat.setDeleted(Boolean.TRUE); + M_ItemForm form = new M_ItemForm(); + form.setItemFormID(11); + form.setItemFormDesc("Tablet"); + form.setModifiedBy("admin"); + form.setDeleted(Boolean.TRUE); + M_Route route = new M_Route(); + route.setRouteID(21); + route.setRouteDesc("Oral"); + route.setModifiedBy("admin"); + route.setDeleted(Boolean.TRUE); + when(itemCategoryRepo.updateItemCategoryDetails(31, "Drugs", "admin")).thenReturn(1); + when(itemCategoryRepo.blockItemCategory(31, Boolean.TRUE, "admin")).thenReturn(1); + when(itemFormRepo.updateItemFormDetails(11, "Tablet", "admin")).thenReturn(1); + when(itemFormRepo.blockItemForm(11, Boolean.TRUE, "admin")).thenReturn(1); + when(routeRepo.updateRouteDetails(21, "Oral", "admin")).thenReturn(1); + when(routeRepo.blockRoute(21, Boolean.TRUE, "admin")).thenReturn(1); + + assertEquals(1, service.editItemCategory(cat)); + assertEquals(1, service.blockItemCategory(cat)); + assertEquals(1, service.editItemForm(form)); + assertEquals(1, service.blockItemForm(form)); + assertEquals(1, service.editRoute(route)); + assertEquals(1, service.blockRoute(route)); + } + + @Test + @DisplayName("the code checks should report a code the provider already uses") + void codeChecks_shouldReportUsedCode() { + CodeChecker checker = new CodeChecker(); + checker.setCode("CODE-1"); + checker.setProviderServiceMapID(PSM_ID); + when(itemCategoryRepo.findByItemCategoryCodeAndProviderServiceMapID("CODE-1", PSM_ID)) + .thenReturn(List.of(category(31))); + when(itemFormRepo.findByItemFormCodeAndProviderServiceMapID("CODE-1", PSM_ID)) + .thenReturn(List.of(new M_ItemForm())); + when(itemRepo.findByItemCodeAndProviderServiceMapID("CODE-1", PSM_ID)) + .thenReturn(List.of(new ItemMaster())); + when(routeRepo.findByRouteCodeAndProviderServiceMapID("CODE-1", PSM_ID)) + .thenReturn(List.of(new M_Route())); + + assertTrue(service.checkCodeCategory(checker)); + assertTrue(service.checkCodeForm(checker)); + assertTrue(service.checkCodeItem(checker)); + assertTrue(service.checkCodeRoute(checker)); + } + + @Test + @DisplayName("the code checks should clear a code nobody uses yet") + void codeChecks_shouldClearFreeCode() { + CodeChecker checker = new CodeChecker(); + checker.setCode("CODE-2"); + checker.setProviderServiceMapID(PSM_ID); + when(itemCategoryRepo.findByItemCategoryCodeAndProviderServiceMapID("CODE-2", PSM_ID)) + .thenReturn(new ArrayList<>()); + when(itemFormRepo.findByItemFormCodeAndProviderServiceMapID("CODE-2", PSM_ID)) + .thenReturn(new ArrayList<>()); + when(itemRepo.findByItemCodeAndProviderServiceMapID("CODE-2", PSM_ID)).thenReturn(new ArrayList<>()); + when(routeRepo.findByRouteCodeAndProviderServiceMapID("CODE-2", PSM_ID)).thenReturn(new ArrayList<>()); + + assertFalse(service.checkCodeCategory(checker)); + assertFalse(service.checkCodeForm(checker)); + assertFalse(service.checkCodeItem(checker)); + assertFalse(service.checkCodeRoute(checker)); + } +} diff --git a/src/test/java/com/iemr/admin/service/itemfacilitymapping/M_itemfacilitymappingImplTest.java b/src/test/java/com/iemr/admin/service/itemfacilitymapping/M_itemfacilitymappingImplTest.java new file mode 100644 index 0000000..6d8cbb5 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/itemfacilitymapping/M_itemfacilitymappingImplTest.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.admin.service.itemfacilitymapping; + +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.admin.data.itemfacilitymapping.M_itemfacilitymapping; +import com.iemr.admin.data.itemfacilitymapping.V_fetchItemFacilityMap; +import com.iemr.admin.data.items.ItemInStore; +import com.iemr.admin.repo.stockEntry.ItemStockEntryRepo; +import com.iemr.admin.repository.itemfacilitymapping.M_itemfacilitymappingRepo; +import com.iemr.admin.repository.itemfacilitymapping.V_fetchItemFacilityMapRepo; + +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.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.when; + +/** + * The item facility service records which items a store is allowed to hold, and + * answers what each store currently has on its shelves. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("M_itemfacilitymappingImpl Test Suite") +class M_itemfacilitymappingImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer FACILITY_ID = 9001; + private static final Integer MAP_ID = 3301; + private static final Integer ITEM_ID = 501; + + @Mock + private V_fetchItemFacilityMapRepo v_fetchItemFacilityMapRepo; + + @Mock + private M_itemfacilitymappingRepo m_itemfacilitymappingRepo; + + @Mock + private ItemStockEntryRepo itemStockEntryRepo; + + @InjectMocks + private M_itemfacilitymappingImpl service; + + private static M_itemfacilitymapping mapping() { + M_itemfacilitymapping mapping = new M_itemfacilitymapping(); + mapping.setItemStoreMapID(MAP_ID); + mapping.setItemID(ITEM_ID); + mapping.setFacilityID(FACILITY_ID); + mapping.setDeleted(Boolean.FALSE); + return mapping; + } + + @Test + @DisplayName("mapItemtoStore should answer the mappings the repository stored") + void map_shouldAnswerStoredMappings() { + ArrayList stored = new ArrayList<>(List.of(mapping())); + when(m_itemfacilitymappingRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.mapItemtoStore(new ArrayList<>())); + } + + @Test + @DisplayName("editdata and saveEditedItem should each reach their own repository query") + void singleRecordOperations_shouldReachTheirOwnQuery() { + M_itemfacilitymapping stored = mapping(); + when(m_itemfacilitymappingRepo.findByItemFacilityMapID(MAP_ID)).thenReturn(stored); + when(m_itemfacilitymappingRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.editdata(MAP_ID)); + assertSame(stored, service.saveEditedItem(stored)); + } + + @Test + @DisplayName("editdata should answer nothing when the mapping is unknown") + void edit_shouldAnswerNothingForUnknownMapping() { + when(m_itemfacilitymappingRepo.findByItemFacilityMapID(-1)).thenReturn(null); + + assertNull(service.editdata(-1)); + } + + @Test + @DisplayName("getsubitemforsubStote should rebuild one item per row the query answers") + void getSubItems_shouldRebuildEachRow() { + when(m_itemfacilitymappingRepo.getItemforSubstore(PSM_ID, FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of( + new Object[] { ITEM_ID, "Paracetamol 500", Boolean.FALSE, 7 }))); + + ArrayList items = service.getsubitemforsubStote(PSM_ID, FACILITY_ID); + + assertEquals(1, items.size()); + assertEquals("Paracetamol 500", items.get(0).getItemName()); + assertEquals(ITEM_ID, items.get(0).getItemID()); + } + + @Test + @DisplayName("getsubitemforsubStote should skip a row the query could not fill in") + void getSubItems_shouldSkipIncompleteRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { ITEM_ID, "Paracetamol 500" }); + when(m_itemfacilitymappingRepo.getItemforSubstore(PSM_ID, FACILITY_ID)).thenReturn(rows); + + assertTrue(service.getsubitemforsubStote(PSM_ID, FACILITY_ID).isEmpty()); + } + + @Test + @DisplayName("the mapped item lookups should each reach their own repository query") + void mappedItemLookups_shouldReachTheirOwnQuery() { + ArrayList byProvider = new ArrayList<>(List.of(new V_fetchItemFacilityMap())); + ArrayList byFacility = new ArrayList<>(List.of(new V_fetchItemFacilityMap())); + when(v_fetchItemFacilityMapRepo.getAllFacilityMappedData(PSM_ID)).thenReturn(byProvider); + when(v_fetchItemFacilityMapRepo.getItemMappingsByFacilityAndSubStores(FACILITY_ID)).thenReturn(byFacility); + + assertSame(byProvider, service.getAllFacilityMappedData(PSM_ID)); + assertSame(byFacility, service.getItemMappingsByFacilityID(FACILITY_ID)); + } + + @Test + @DisplayName("getItemMastersFromStoreID should answer what the store holds of each item mapped to it") + void getItemMasters_shouldAnswerWhatStoreHolds() { + when(m_itemfacilitymappingRepo.getItemforStore(FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(new Object[] { ITEM_ID, "Paracetamol 500" }))); + when(itemStockEntryRepo.getQuantity(any(Integer[].class), anyInt())) + .thenReturn(new ArrayList<>(List.of( + new Object[] { FACILITY_ID, ITEM_ID, "Paracetamol 500", 250L }))); + + List held = service.getItemMastersFromStoreID(FACILITY_ID); + + assertEquals(1, held.size()); + assertEquals("Paracetamol 500", held.get(0).getItemName()); + assertEquals(250L, held.get(0).getQuantity()); + } + + @Test + @DisplayName("getItemMastersFromStoreID should answer nothing when the store holds none of its items") + void getItemMasters_shouldAnswerNothingWhenStoreEmpty() { + when(m_itemfacilitymappingRepo.getItemforStore(FACILITY_ID)).thenReturn(new ArrayList<>()); + when(itemStockEntryRepo.getQuantity(any(Integer[].class), anyInt())).thenReturn(new ArrayList<>()); + + assertTrue(service.getItemMastersFromStoreID(FACILITY_ID).isEmpty()); + } + + @Test + @DisplayName("deleteItemStoreMapping should report how many mappings the retirement touched") + void delete_shouldReportRowsTouched() { + M_itemfacilitymapping request = mapping(); + request.setDeleted(Boolean.TRUE); + when(m_itemfacilitymappingRepo.updateDeleteMap(MAP_ID, Boolean.TRUE)).thenReturn(1); + + assertEquals(1, service.deleteItemStoreMapping(request)); + } + + @Test + @DisplayName("deleteItemStoreMapping should report nothing touched when the mapping is unknown") + void delete_shouldReportNothingTouchedForUnknownMapping() { + M_itemfacilitymapping request = new M_itemfacilitymapping(); + request.setItemStoreMapID(-1); + when(m_itemfacilitymappingRepo.updateDeleteMap(-1, null)).thenReturn(0); + + assertEquals(0, service.deleteItemStoreMapping(request)); + } +} diff --git a/src/test/java/com/iemr/admin/service/locationmaster/LocationMasterServiceImplTest.java b/src/test/java/com/iemr/admin/service/locationmaster/LocationMasterServiceImplTest.java new file mode 100644 index 0000000..bdc8b14 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/locationmaster/LocationMasterServiceImplTest.java @@ -0,0 +1,180 @@ +/* +* 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.admin.service.locationmaster; + +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.admin.data.locationmaster.M_District; +import com.iemr.admin.data.locationmaster.M_ProviderServiceAddMapping; +import com.iemr.admin.data.locationmaster.Showofficedetails; +import com.iemr.admin.data.locationmaster.StateServiceMapping1; +import com.iemr.admin.repo.locationmaster.LocationMasterRepo; +import com.iemr.admin.repo.locationmaster.MdistrictRepo; +import com.iemr.admin.repo.locationmaster.M_ProviderServiceAddMappingRepo; +import com.iemr.admin.repo.locationmaster.ShowofficedetailsRepo; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.when; + +/** + * The location service reads a provider's office addresses out of several + * differently shaped queries and reshapes them into one carrier. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("LocationMasterServiceImpl Test Suite") +class LocationMasterServiceImplTest { + + private static final Integer PROVIDER_ID = 77; + private static final Integer PSM_ID = 4001; + + @Mock + private ShowofficedetailsRepo showofficedetailsRepo; + + @Mock + private MdistrictRepo mdistricRepo; + + @Mock + private M_ProviderServiceAddMappingRepo m_ProviderServiceAddMappingRepo; + + @Mock + private LocationMasterRepo locationMasterRepo; + + @InjectMocks + private LocationMasterServiceImpl service; + + @Test + @DisplayName("getStateByServiceProviderId should skip a row the query could not fill") + void getStateByServiceProviderId_shouldSkipUnfillableRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { 29, "Karnataka", 1, PSM_ID }); + when(locationMasterRepo.getStateByServiceProviderId(PROVIDER_ID)).thenReturn(rows); + + assertEquals(1, service.getStateByServiceProviderId(PROVIDER_ID).size()); + } + + @Test + @DisplayName("getServiceByServiceProviderIdAndStateId should rebuild one mapping per row") + void getServiceByServiceProviderIdAndStateId_shouldRebuildEachRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { 3, PSM_ID, "Tele Medicine" }); + when(locationMasterRepo.getServiceByServiceProviderIdAndStateId(PROVIDER_ID, 29)).thenReturn(rows); + + assertEquals(1, service.getServiceByServiceProviderIdAndStateId(PROVIDER_ID, 29).size()); + } + + @Test + @DisplayName("getStatesByServiceId should rebuild one mapping per row") + void getStatesByServiceId_shouldRebuildEachRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { 29, "Karnataka", PSM_ID }); + when(locationMasterRepo.getStatesByServiceId(3, PROVIDER_ID)).thenReturn(rows); + + assertEquals(1, service.getStatesByServiceId(3, PROVIDER_ID).size()); + } + + @Test + @DisplayName("getAllDistrictByStateId should rebuild each district into a plain carrier") + void getAllDistrictByStateId_shouldRebuildEachDistrict() { + M_District stored = new M_District(); + stored.setDistrictID(301); + stored.setDistrictName("Bengaluru Urban"); + when(mdistricRepo.getAllDistrictByStateId(29)).thenReturn(new ArrayList<>(List.of(stored))); + + ArrayList districts = service.getAllDistrictByStateId(29); + + assertEquals(1, districts.size()); + assertEquals("Bengaluru Urban", districts.get(0).getDistrictName()); + } + + @Test + @DisplayName("getlocationByMapid1 should gather the offices of every mapping the caller lists") + void getlocationByMapid1_shouldGatherAcrossMappings() { + Showofficedetails office = new Showofficedetails(); + when(showofficedetailsRepo.getlocationByMapid1(4001)).thenReturn(new ArrayList<>(List.of(office))); + when(showofficedetailsRepo.getlocationByMapid1(4002)).thenReturn(new ArrayList<>(List.of(office))); + + assertEquals(2, service.getlocationByMapid1(new ArrayList<>(List.of(4001, 4002))).size()); + } + + @Test + @DisplayName("getOfficeName should gather the office of every mapping the caller lists") + void getOfficeName_shouldGatherAcrossMappings() { + Showofficedetails request = new Showofficedetails(); + request.setProviderServiceMapID(PSM_ID); + when(showofficedetailsRepo.getOfficeName(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(new Showofficedetails()))); + + assertEquals(1, service.getOfficeName(new ArrayList<>(List.of(request))).size()); + } + + @Test + @DisplayName("the remaining calls should each reach their own repository query") + void remainingCalls_shouldReachTheirOwnQuery() { + M_ProviderServiceAddMapping address = new M_ProviderServiceAddMapping(); + ArrayList addresses = new ArrayList<>(List.of(address)); + ArrayList offices = new ArrayList<>(List.of(new Showofficedetails())); + StateServiceMapping1 mapping = new StateServiceMapping1(PSM_ID); + ArrayList mappings = new ArrayList<>(List.of(mapping)); + when(m_ProviderServiceAddMappingRepo.save(address)).thenReturn(address); + when(m_ProviderServiceAddMappingRepo.saveAll(anyList())).thenReturn(addresses); + when(m_ProviderServiceAddMappingRepo.editData(51)).thenReturn(address); + when(m_ProviderServiceAddMappingRepo.getlocationByMapid(PSM_ID)).thenReturn(addresses); + when(showofficedetailsRepo.getAlldata()).thenReturn(offices); + when(showofficedetailsRepo.getlocationByMapid(PSM_ID)).thenReturn(offices); + when(showofficedetailsRepo.getlocationByMapid3(PSM_ID, 301)).thenReturn(offices); + when(locationMasterRepo.getProviderServiceMapID(PROVIDER_ID, 29, 3)).thenReturn(mapping); + when(locationMasterRepo.getAllByMapId2(PROVIDER_ID, 29, 3)).thenReturn(mappings); + when(locationMasterRepo.getAllByMapId3(PROVIDER_ID, 3)).thenReturn(mappings); + when(locationMasterRepo.getLocationByServiceID(PROVIDER_ID, 3)).thenReturn(mappings); + when(locationMasterRepo.getLocationByStateID(PROVIDER_ID, 29)).thenReturn(mappings); + + assertSame(address, service.addlocation(address)); + assertSame(addresses, service.addlocation(new ArrayList<>())); + assertSame(address, service.editData(51)); + assertSame(address, service.saveEditData(address)); + assertSame(addresses, service.getlocationByMapid(PSM_ID)); + assertSame(offices, service.getAlldata()); + assertSame(offices, service.getlocationByMapid2(PSM_ID)); + assertSame(offices, service.getlocationByMapid4(PSM_ID, 301)); + assertSame(mapping, service.getAllByMapId(PROVIDER_ID, 29, 3)); + assertSame(mappings, service.getAllByMapId2(PROVIDER_ID, 29, 3)); + assertSame(mappings, service.getAllByMapId3(PROVIDER_ID, 3)); + assertSame(mappings, service.getLocationByServiceId(PROVIDER_ID, 3)); + assertSame(mappings, service.getLocationBySateID(PROVIDER_ID, 29)); + } +} diff --git a/src/test/java/com/iemr/admin/service/manufacturer/ManufacturerServiceImplTest.java b/src/test/java/com/iemr/admin/service/manufacturer/ManufacturerServiceImplTest.java new file mode 100644 index 0000000..a473821 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/manufacturer/ManufacturerServiceImplTest.java @@ -0,0 +1,113 @@ +/* +* 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.admin.service.manufacturer; + +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.admin.data.manufacturer.M_Manufacturer; +import com.iemr.admin.repo.manufacturer.ManufacturerRepo; + +import static org.junit.jupiter.api.Assertions.assertFalse; +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.anyList; +import static org.mockito.Mockito.when; + +/** The manufacturer service is thin over its repository, but it answers nothing rather than an empty batch. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ManufacturerServiceImpl Test Suite") +class ManufacturerServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer RECORD_ID = 11; + + @Mock + private ManufacturerRepo manufacturerRepo; + + @InjectMocks + private ManufacturerServiceImpl service; + + @Test + @DisplayName("createManufacturer should answer the records it stored") + void create_shouldAnswerStoredRecords() { + ArrayList stored = new ArrayList<>(List.of(new M_Manufacturer())); + when(manufacturerRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.createManufacturer(new ArrayList<>())); + } + + @Test + @DisplayName("createManufacturer should answer nothing when the store took nothing") + void create_shouldAnswerNothingWhenNothingStored() { + when(manufacturerRepo.saveAll(anyList())).thenReturn(new ArrayList()); + + assertNull(service.createManufacturer(new ArrayList<>())); + } + + @Test + @DisplayName("the lookups should each reach their own repository query") + void lookups_shouldReachTheirOwnQuery() { + M_Manufacturer record = new M_Manufacturer(); + ArrayList records = new ArrayList<>(List.of(record)); + when(manufacturerRepo.getManufacturerData(PSM_ID)).thenReturn(records); + when(manufacturerRepo.getEditData(RECORD_ID)).thenReturn(record); + when(manufacturerRepo.save(record)).thenReturn(record); + + assertSame(records, service.createManufacturer(PSM_ID)); + assertSame(record, service.editManufacturer(RECORD_ID)); + assertSame(record, service.saveEditedData(record)); + } + + @Test + @DisplayName("checkManufacturerCode should report a code the provider already uses") + void check_shouldReportUsedCode() { + M_Manufacturer request = new M_Manufacturer(); + request.setManufacturerCode("C-1"); + request.setProviderServiceMapID(PSM_ID); + when(manufacturerRepo.findByManufacturerCodeAndProviderServiceMapID("C-1", PSM_ID)).thenReturn(List.of(new M_Manufacturer())); + + assertTrue(service.checkManufacturerCode(request)); + } + + @Test + @DisplayName("checkManufacturerCode should clear a code nobody uses yet") + void check_shouldClearFreeCode() { + M_Manufacturer request = new M_Manufacturer(); + request.setManufacturerCode("C-2"); + request.setProviderServiceMapID(PSM_ID); + when(manufacturerRepo.findByManufacturerCodeAndProviderServiceMapID("C-2", PSM_ID)).thenReturn(new ArrayList()); + + assertFalse(service.checkManufacturerCode(request)); + } +} diff --git a/src/test/java/com/iemr/admin/service/nodalemailconfig/NodalConfigServiceImplTest.java b/src/test/java/com/iemr/admin/service/nodalemailconfig/NodalConfigServiceImplTest.java new file mode 100644 index 0000000..68d6667 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/nodalemailconfig/NodalConfigServiceImplTest.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.admin.service.nodalemailconfig; + +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.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.admin.data.emailconfig.AuthorityEmail; +import com.iemr.admin.mapper.emailconfig.InstituteEmailConfigMapper; +import com.iemr.admin.model.emailconfig.NodalEmailRequest; +import com.iemr.admin.model.emailconfig.NodalEmailResponse; +import com.iemr.admin.model.emailconfig.CreateNodalEmailRequestModel; +import com.iemr.admin.model.emailconfig.UpdateNodalEmailRequest; +import com.iemr.admin.repository.emailconfig.InstituteEmailRepo; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The nodal config service keeps the nodal officer mailboxes a complaint is + * escalated to, narrowed by whichever parts of the location the caller names. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("NodalConfigServiceImpl Test Suite") +class NodalConfigServiceImplTest { + + @Mock + private EntityManager entityManager; + + @Mock + private InstituteEmailRepo instituteRepo; + + @Mock + private InstituteEmailConfigMapper instituteEmailConfigMapper; + + @InjectMocks + private NodalConfigServiceImpl service; + + private CriteriaQuery query; + private TypedQuery typedQuery; + + @SuppressWarnings("unchecked") + @BeforeEach + @DisplayName("Stand in for the criteria query the service builds by hand") + void setUp() { + CriteriaBuilder builder = mock(CriteriaBuilder.class); + query = mock(CriteriaQuery.class); + Root root = mock(Root.class); + typedQuery = mock(TypedQuery.class); + + when(entityManager.getCriteriaBuilder()).thenReturn(builder); + when(builder.createQuery(AuthorityEmail.class)).thenReturn(query); + when(query.from(AuthorityEmail.class)).thenReturn(root); + when(query.select(any())).thenReturn(query); + when(query.where(any(Predicate[].class))).thenReturn(query); + when(query.orderBy(any(jakarta.persistence.criteria.Order[].class))).thenReturn(query); + when(root.get(anyString())).thenReturn(mock(Path.class)); + when(builder.equal(any(), any())).thenReturn(mock(Predicate.class)); + when(entityManager.createQuery(query)).thenReturn(typedQuery); + } + + private static NodalEmailRequest fullyNarrowedRequest() { + NodalEmailRequest request = new NodalEmailRequest(); + request.setAuthorityEmailID(1); + request.setDeleted(false); + request.setDistrictID(301); + request.setDistrictBranchMappingID(30111); + request.setBlockID(3011); + request.setProviderServiceMapID(4001); + request.setStateID(29); + request.setMobileNo("9000000001"); + return request; + } + + @Test + @DisplayName("getAllEmailConfigs should answer the mailboxes the query found, as the screens read them") + void getAll_shouldAnswerFoundMailboxes() { + List found = List.of(new AuthorityEmail()); + List published = List.of(new NodalEmailResponse()); + when(typedQuery.getResultList()).thenReturn(found); + when(instituteEmailConfigMapper.resultToInstTypeEmailResponses(found)).thenReturn(published); + + assertSame(published, service.getAllNodalEmailConfigs(fullyNarrowedRequest())); + } + + @Test + @DisplayName("getAllEmailConfigs should narrow the query by every detail the caller named") + void getAll_shouldNarrowByEveryNamedDetail() { + when(typedQuery.getResultList()).thenReturn(new ArrayList<>()); + + service.getAllNodalEmailConfigs(fullyNarrowedRequest()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Predicate[].class); + verify(query).where(captor.capture()); + assertEquals(8, captor.getValue().length, "one narrowing per detail the caller named"); + } + + @Test + @DisplayName("getAllEmailConfigs should narrow by nothing when the caller names nothing") + void getAll_shouldNarrowByNothingForEmptyRequest() { + when(typedQuery.getResultList()).thenReturn(new ArrayList<>()); + + service.getAllNodalEmailConfigs(new NodalEmailRequest()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Predicate[].class); + verify(query).where(captor.capture()); + assertEquals(0, captor.getValue().length); + } + + @Test + @DisplayName("saveEmailConfigs should store one mailbox per request and answer each as stored") + void save_shouldStoreEachRequestedMailbox() { + AuthorityEmail stored = new AuthorityEmail(); + when(instituteEmailConfigMapper.createRequestToInstituteEmailConfig(anyList())) + .thenReturn(List.of(stored, stored)); + when(instituteRepo.save(stored)).thenReturn(stored); + when(instituteEmailConfigMapper.resultInstType(stored)) + .thenReturn(new NodalEmailResponse()); + + assertEquals(2, service.saveNodalEmailConfigs(List.of(new CreateNodalEmailRequestModel())).size()); + } + + @Test + @DisplayName("saveEmailConfigs should store nothing when the caller asks for nothing") + void save_shouldStoreNothingForEmptyRequest() { + when(instituteEmailConfigMapper.createRequestToInstituteEmailConfig(anyList())) + .thenReturn(new ArrayList<>()); + + assertTrue(service.saveNodalEmailConfigs(new ArrayList<>()).isEmpty()); + } + + @Test + @DisplayName("updateEmailConfigs should answer the mailbox as it stands after the change") + void update_shouldAnswerChangedMailbox() { + AuthorityEmail stored = new AuthorityEmail(); + NodalEmailResponse published = new NodalEmailResponse(); + when(instituteEmailConfigMapper.updateRequestToInstituteNodalEmailConf(any(UpdateNodalEmailRequest.class))) + .thenReturn(stored); + when(instituteRepo.save(stored)).thenReturn(stored); + when(instituteEmailConfigMapper.resultToInstTypeNodalEmailResponse(stored)).thenReturn(published); + + assertSame(published, service.updateNodalEmailConfigs(new UpdateNodalEmailRequest())); + } +} diff --git a/src/test/java/com/iemr/admin/service/parkingPlace/ParkingPlaceServiceImplTest.java b/src/test/java/com/iemr/admin/service/parkingPlace/ParkingPlaceServiceImplTest.java new file mode 100644 index 0000000..8065453 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/parkingPlace/ParkingPlaceServiceImplTest.java @@ -0,0 +1,184 @@ +/* +* 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.admin.service.parkingPlace; + +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.admin.data.parkingPlace.M_Parkingplace; +import com.iemr.admin.data.provideronboard.M_ProviderServiceMapping; +import com.iemr.admin.data.zonemaster.M_Zone; +import com.iemr.admin.repository.parkingPlace.ParkingPlaceRepository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The parking place service turns the flat rows the reporting queries answer + * back into parking places, and treats an omitted location filter as "any". + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ParkingPlaceServiceImpl Test Suite") +class ParkingPlaceServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer PARKING_PLACE_ID = 31; + + @Mock + private ParkingPlaceRepository parkingPlaceRepository; + + @InjectMocks + private ParkingPlaceServiceImpl service; + + private static Object[] reportingRow() { + return new Object[] { PARKING_PLACE_ID, "Hosur parking", "Near the bus stand", "Hosur Road", PSM_ID, + Boolean.FALSE, 1, "India", 29, "Karnataka", 301, "Bengaluru Urban", 3011, "Anekal", 30111, + "Attibele", new M_ProviderServiceMapping(), 5, "104 Helpline" }; + } + + @Test + @DisplayName("getAvailableParkingPlaces should rebuild one parking place per row the query answers") + void getAvailable_shouldRebuildEachRow() { + when(parkingPlaceRepository.getAvailableParkingPlaces("29", "301", PSM_ID)) + .thenReturn(List.of(reportingRow())); + + ArrayList places = service.getAvailableParkingPlaces(29, 301, PSM_ID); + + assertEquals(1, places.size()); + assertEquals("Hosur parking", places.get(0).getParkingPlaceName()); + assertEquals("Karnataka", places.get(0).getStateName()); + assertEquals("Hosur Road", places.get(0).getAreaHQAddress()); + } + + @Test + @DisplayName("getAvailableParkingPlaces should match any state or district the caller leaves out") + void getAvailable_shouldWildcardOmittedFilters() { + when(parkingPlaceRepository.getAvailableParkingPlaces("%%", "%%", PSM_ID)).thenReturn(new ArrayList<>()); + + assertTrue(service.getAvailableParkingPlaces(null, null, PSM_ID).isEmpty()); + verify(parkingPlaceRepository).getAvailableParkingPlaces("%%", "%%", PSM_ID); + } + + @Test + @DisplayName("saveParkingPlace should answer the parking places the repository stored") + void save_shouldAnswerStoredPlaces() { + ArrayList stored = new ArrayList<>(List.of(new M_Parkingplace())); + when(parkingPlaceRepository.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.saveParkingPlace(new ArrayList<>())); + } + + @Test + @DisplayName("updateParkingPlaceStatus should report how many rows the retirement touched") + void updateStatus_shouldReportRowsTouched() { + M_Parkingplace request = new M_Parkingplace(); + request.setParkingPlaceID(PARKING_PLACE_ID); + request.setDeleted(Boolean.TRUE); + request.setModifiedBy("admin"); + when(parkingPlaceRepository.updateParkingPlaceStatus(PARKING_PLACE_ID, Boolean.TRUE, "admin")).thenReturn(1); + + assertEquals(1, service.updateParkingPlaceStatus(request)); + } + + @Test + @DisplayName("updateParkingPlaceStatus should report nothing touched when the parking place is unknown") + void updateStatus_shouldReportNothingTouchedForUnknownPlace() { + M_Parkingplace request = new M_Parkingplace(); + request.setParkingPlaceID(-1); + when(parkingPlaceRepository.updateParkingPlaceStatus(-1, null, null)).thenReturn(0); + + assertEquals(0, service.updateParkingPlaceStatus(request)); + } + + @Test + @DisplayName("the single record lookups should each reach their own repository query") + void singleRecordLookups_shouldReachTheirOwnQuery() { + M_Parkingplace stored = new M_Parkingplace(); + List byProvider = List.of(stored); + when(parkingPlaceRepository.getParkingPlaceById(PARKING_PLACE_ID)).thenReturn(stored); + when(parkingPlaceRepository.save(stored)).thenReturn(stored); + when(parkingPlaceRepository.findByProviderServiceMapID(PSM_ID)).thenReturn(byProvider); + + assertSame(stored, service.getParkingPlaceByID(PARKING_PLACE_ID)); + assertSame(stored, service.updateParkingPlaceData(stored)); + assertSame(byProvider, service.getParkingPlaces(PSM_ID)); + } + + @Test + @DisplayName("getSubDistrict should answer the taluks the parking place covers") + void getSubDistrict_shouldAnswerCoveredTaluks() { + when(parkingPlaceRepository.getSubDistrict(PARKING_PLACE_ID)) + .thenReturn(List.of(new Object[] { PARKING_PLACE_ID, 3011, "Anekal" })); + + List taluks = service.getSubDistrict(PARKING_PLACE_ID); + + assertEquals(1, taluks.size()); + assertEquals("Anekal", taluks.get(0).getBlockName()); + assertEquals(3011, taluks.get(0).getDistrictBlockID()); + } + + @Test + @DisplayName("getSubDistrict should answer nothing when the parking place covers no taluk") + void getSubDistrict_shouldAnswerNothingWhenNoneCovered() { + when(parkingPlaceRepository.getSubDistrict(PARKING_PLACE_ID)).thenReturn(new ArrayList<>()); + + assertTrue(service.getSubDistrict(PARKING_PLACE_ID).isEmpty()); + } + + @Test + @DisplayName("getAvailableParkingPlacesbyZoneID should name the zone on each parking place it answers") + void getAvailableByZone_shouldNameTheZone() { + M_Parkingplace place = new M_Parkingplace(); + place.setParkingPlaceID(PARKING_PLACE_ID); + M_Zone zone = new M_Zone(); + zone.setZoneName("South zone"); + when(parkingPlaceRepository.getAvailableParkingPlacesbyzoneid(9, PSM_ID)) + .thenReturn(List.of(new Object[] { place, zone })); + + ArrayList places = service.getAvailableParkingPlacesbyZoneID(9, PSM_ID); + + assertEquals(1, places.size()); + assertEquals("South zone", places.get(0).getZoneName()); + } + + @Test + @DisplayName("getAvailableParkingPlacesbyZoneID should answer nothing when the zone holds no parking place") + void getAvailableByZone_shouldAnswerNothingForEmptyZone() { + when(parkingPlaceRepository.getAvailableParkingPlacesbyzoneid(9, PSM_ID)).thenReturn(new ArrayList<>()); + + assertTrue(service.getAvailableParkingPlacesbyZoneID(9, PSM_ID).isEmpty()); + } +} diff --git a/src/test/java/com/iemr/admin/service/parkingPlace/ParkingPlaceTalukMappingServiceImplTest.java b/src/test/java/com/iemr/admin/service/parkingPlace/ParkingPlaceTalukMappingServiceImplTest.java new file mode 100644 index 0000000..24df775 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/parkingPlace/ParkingPlaceTalukMappingServiceImplTest.java @@ -0,0 +1,151 @@ +/* +* 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.admin.service.parkingPlace; + +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.admin.data.locationmaster.DistrictBlock; +import com.iemr.admin.data.parkingPlace.ParkingplaceTalukMapping; +import com.iemr.admin.data.parkingPlace.ParkingplaceTalukMappingTO; +import com.iemr.admin.mapper.parkingplacetalukmapping.ParkingPlaceTalukMappingMapper; +import com.iemr.admin.repo.locationmaster.DistrictBlockRepo; +import com.iemr.admin.repository.parkingPlace.ParkingPlaceTalukMappingRepository; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The taluk mapping service records which taluks a parking place covers, and + * offers the remaining taluks of a district as the candidates for a new one. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ParkingPlaceTalukMappingServiceImpl Test Suite") +class ParkingPlaceTalukMappingServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer PARKING_PLACE_ID = 31; + private static final Integer DISTRICT_ID = 301; + + @Mock + private ParkingPlaceTalukMappingRepository parkingPlaceTalukMappingRepository; + + @Mock + private DistrictBlockRepo districtBlockRepo; + + @Mock + private ParkingPlaceTalukMappingMapper parkingPlaceTalukMappingMapper; + + @InjectMocks + private ParkingPlaceTalukMappingServiceImpl service; + + private static ParkingplaceTalukMapping mapping() { + ParkingplaceTalukMapping mapping = new ParkingplaceTalukMapping(); + mapping.setPpSubDistrictMapID(7001); + mapping.setParkingPlaceID(PARKING_PLACE_ID); + mapping.setDistrictID(DISTRICT_ID); + mapping.setDistrictBlockID(3011); + mapping.setProviderServiceMapID(PSM_ID); + return mapping; + } + + @Test + @DisplayName("saveParkingPlaceTalukMapping should answer the mappings the repository stored") + void save_shouldAnswerStoredMappings() { + ArrayList stored = new ArrayList<>(List.of(mapping())); + when(parkingPlaceTalukMappingRepository.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.saveParkingPlaceTalukMapping(new ArrayList<>())); + } + + @Test + @DisplayName("updateParkingPlaceTalukMapping and findbyID should each reach their own repository query") + void singleRecordOperations_shouldReachTheirOwnQuery() { + ParkingplaceTalukMapping stored = mapping(); + when(parkingPlaceTalukMappingRepository.save(stored)).thenReturn(stored); + when(parkingPlaceTalukMappingRepository.findByPpSubDistrictMapID(7001)).thenReturn(stored); + + assertSame(stored, service.updateParkingPlaceTalukMapping(stored)); + assertSame(stored, service.findbyID(7001)); + } + + @Test + @DisplayName("findbyProviderservicemapid should answer the taluks the parking place covers") + void findbyProviderservicemapid_shouldAnswerCoveredTaluks() { + List rows = List.of(mapping()); + List published = List.of(new ParkingplaceTalukMappingTO()); + when(parkingPlaceTalukMappingRepository.findByParkingPlaceID(PARKING_PLACE_ID)).thenReturn(rows); + when(parkingPlaceTalukMappingMapper.getParkingplaceTalukMappingMapList(rows)).thenReturn(published); + + assertSame(published, service.findbyProviderservicemapid(mapping())); + } + + @Test + @DisplayName("findbyParkingplaceAndDistrictID should narrow the mappings to the district the caller names") + void findbyParkingplaceAndDistrictID_shouldNarrowToDistrict() { + List rows = List.of(mapping()); + List published = List.of(new ParkingplaceTalukMappingTO()); + when(parkingPlaceTalukMappingRepository + .findByParkingPlaceIDAndDistrictIDOrderByM_DistrictDistrictNameAsc(PARKING_PLACE_ID, DISTRICT_ID)) + .thenReturn(rows); + when(parkingPlaceTalukMappingMapper.getParkingplaceTalukMappingMapList(rows)).thenReturn(published); + + assertSame(published, service.findbyParkingplaceAndDistrictID(mapping())); + } + + @Test + @DisplayName("getunmappedtaluk should exclude the taluks already covered when there are any") + void getunmappedtaluk_shouldExcludeCoveredTaluks() { + List covered = List.of(3011); + List remaining = List.of(new DistrictBlock(3012, "Hoskote")); + when(parkingPlaceTalukMappingRepository.finbyDistrictID(DISTRICT_ID, PSM_ID)).thenReturn(covered); + when(districtBlockRepo.findunmapped(covered, DISTRICT_ID)).thenReturn(remaining); + + assertSame(remaining, service.getunmappedtaluk(DISTRICT_ID, PSM_ID)); + verify(districtBlockRepo, never()).findall(anyInt()); + } + + @Test + @DisplayName("getunmappedtaluk should offer every taluk of the district when none is covered yet") + void getunmappedtaluk_shouldOfferEveryTalukWhenNoneCovered() { + List all = List.of(new DistrictBlock(3011, "Anekal"), new DistrictBlock(3012, "Hoskote")); + when(parkingPlaceTalukMappingRepository.finbyDistrictID(DISTRICT_ID, PSM_ID)).thenReturn(new ArrayList<>()); + when(districtBlockRepo.findall(DISTRICT_ID)).thenReturn(all); + + assertSame(all, service.getunmappedtaluk(DISTRICT_ID, PSM_ID)); + verify(districtBlockRepo, never()).findunmapped(anyList(), anyInt()); + } +} diff --git a/src/test/java/com/iemr/admin/service/pharmacologicalcategory/PharmacologicalcategoryServiceImplTest.java b/src/test/java/com/iemr/admin/service/pharmacologicalcategory/PharmacologicalcategoryServiceImplTest.java new file mode 100644 index 0000000..9286283 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/pharmacologicalcategory/PharmacologicalcategoryServiceImplTest.java @@ -0,0 +1,113 @@ +/* +* 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.admin.service.pharmacologicalcategory; + +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.admin.data.pharmacologicalcategory.M_Pharmacologicalcategory; +import com.iemr.admin.repo.pharmacologicalcategory.PharmacologicalcategoryRepo; + +import static org.junit.jupiter.api.Assertions.assertFalse; +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.anyList; +import static org.mockito.Mockito.when; + +/** The pharmacological category service is thin over its repository, but it answers nothing rather than an empty batch. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("PharmacologicalcategoryServiceImpl Test Suite") +class PharmacologicalcategoryServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer RECORD_ID = 11; + + @Mock + private PharmacologicalcategoryRepo pharmacologicalcategoryRepo; + + @InjectMocks + private PharmacologicalcategoryServiceImpl service; + + @Test + @DisplayName("createPharmacologicalcategory should answer the records it stored") + void create_shouldAnswerStoredRecords() { + ArrayList stored = new ArrayList<>(List.of(new M_Pharmacologicalcategory())); + when(pharmacologicalcategoryRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.createPharmacologicalcategory(new ArrayList<>())); + } + + @Test + @DisplayName("createPharmacologicalcategory should answer nothing when the store took nothing") + void create_shouldAnswerNothingWhenNothingStored() { + when(pharmacologicalcategoryRepo.saveAll(anyList())).thenReturn(new ArrayList()); + + assertNull(service.createPharmacologicalcategory(new ArrayList<>())); + } + + @Test + @DisplayName("the lookups should each reach their own repository query") + void lookups_shouldReachTheirOwnQuery() { + M_Pharmacologicalcategory record = new M_Pharmacologicalcategory(); + ArrayList records = new ArrayList<>(List.of(record)); + when(pharmacologicalcategoryRepo.getPhormacologicalData(PSM_ID)).thenReturn(records); + when(pharmacologicalcategoryRepo.editPhamacologicalData(RECORD_ID)).thenReturn(record); + when(pharmacologicalcategoryRepo.save(record)).thenReturn(record); + + assertSame(records, service.getPharmacologicalcategory(PSM_ID)); + assertSame(record, service.editPharmacologicalcategory(RECORD_ID)); + assertSame(record, service.saveEditedPharData(record)); + } + + @Test + @DisplayName("checkPharmacologicalcategoryCode should report a code the provider already uses") + void check_shouldReportUsedCode() { + M_Pharmacologicalcategory request = new M_Pharmacologicalcategory(); + request.setPharmCategoryCode("C-1"); + request.setProviderServiceMapID(PSM_ID); + when(pharmacologicalcategoryRepo.findByPharmCategoryCodeAndProviderServiceMapID("C-1", PSM_ID)).thenReturn(List.of(new M_Pharmacologicalcategory())); + + assertTrue(service.checkPharmacologicalcategoryCode(request)); + } + + @Test + @DisplayName("checkPharmacologicalcategoryCode should clear a code nobody uses yet") + void check_shouldClearFreeCode() { + M_Pharmacologicalcategory request = new M_Pharmacologicalcategory(); + request.setPharmCategoryCode("C-2"); + request.setProviderServiceMapID(PSM_ID); + when(pharmacologicalcategoryRepo.findByPharmCategoryCodeAndProviderServiceMapID("C-2", PSM_ID)).thenReturn(new ArrayList()); + + assertFalse(service.checkPharmacologicalcategoryCode(request)); + } +} diff --git a/src/test/java/com/iemr/admin/service/provideronboard/ProviderOnBoardServicesTest.java b/src/test/java/com/iemr/admin/service/provideronboard/ProviderOnBoardServicesTest.java new file mode 100644 index 0000000..516cd48 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/provideronboard/ProviderOnBoardServicesTest.java @@ -0,0 +1,801 @@ +/* +* 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.admin.service.provideronboard; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +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.google.gson.JsonObject; +import com.iemr.admin.data.provideronboard.M_104druggroup; +import com.iemr.admin.data.provideronboard.M_104drugmapping; +import com.iemr.admin.data.provideronboard.M_104drugmaster; +import com.iemr.admin.data.provideronboard.M_Calltype; +import com.iemr.admin.data.provideronboard.M_Category; +import com.iemr.admin.data.provideronboard.M_Feedbacknature; +import com.iemr.admin.data.provideronboard.M_Feedbacktype; +import com.iemr.admin.data.provideronboard.M_Institutedirectory; +import com.iemr.admin.data.provideronboard.M_Institutedirectorymapping; +import com.iemr.admin.data.provideronboard.M_Institutesubdirectory; +import com.iemr.admin.data.provideronboard.M_Institution; +import com.iemr.admin.data.provideronboard.M_Institutiontype; +import com.iemr.admin.data.provideronboard.M_ProviderServiceMapping; +import com.iemr.admin.data.provideronboard.M_ServiceMaster; +import com.iemr.admin.data.provideronboard.M_Severity; +import com.iemr.admin.data.provideronboard.M_Subcategory; +import com.iemr.admin.data.provideronboard.M_Subservice; +import com.iemr.admin.data.provideronboard.M_SubservicemasterPA; +import com.iemr.admin.data.provideronboard.M_UserservicerolemappingForRole; +import com.iemr.admin.data.provideronboard.ServiceProvider_Model; +import com.iemr.admin.data.provideronboard.V_Showprovideradmin; +import com.iemr.admin.data.provideronboard.V_Showsubcategory; +import com.iemr.admin.exceptionhandler.DataNotFound; +import com.iemr.admin.repository.provideronboard.CalltypeRepo; +import com.iemr.admin.repository.provideronboard.CategoryRepo; +import com.iemr.admin.repository.provideronboard.DrugGroupRepo; +import com.iemr.admin.repository.provideronboard.DrugMappingRepo; +import com.iemr.admin.repository.provideronboard.DrugMasterRepo; +import com.iemr.admin.repository.provideronboard.IemrServiceRepository1; +import com.iemr.admin.repository.provideronboard.InstuteDirectoryRepo; +import com.iemr.admin.repository.provideronboard.M_FeedbacknatureRepo; +import com.iemr.admin.repository.provideronboard.M_FeedbacktypeRepo; +import com.iemr.admin.repository.provideronboard.M_InstitutedirectorymappingRepo; +import com.iemr.admin.repository.provideronboard.M_InstitutesubdirectoryRepo; +import com.iemr.admin.repository.provideronboard.M_InstitutionRepo; +import com.iemr.admin.repository.provideronboard.M_InstitutiontypeRepo; +import com.iemr.admin.repository.provideronboard.M_ProviderServiceMappingRepo; +import com.iemr.admin.repository.provideronboard.M_ServiceMasterRepo; +import com.iemr.admin.repository.provideronboard.M_SeverityRepo; +import com.iemr.admin.repository.provideronboard.M_SubservicemasterPArepo; +import com.iemr.admin.repository.provideronboard.M_UserservicerolemappingForRoleRepo; +import com.iemr.admin.repository.provideronboard.SubCategoryRepo; +import com.iemr.admin.repository.provideronboard.SubserviceMasterRepo; +import com.iemr.admin.repository.provideronboard.V_ShowprovideradminRepo; +import com.iemr.admin.repository.provideronboard.V_ShowsubcategoryRepo; + +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.assertThrows; +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; + +/** + * The onboarding services are thin over their repositories, but a few of them + * choose which query to run from what the caller left blank - and those choices + * decide what an operator sees on screen. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("Provider onboarding service Test Suite") +class ProviderOnBoardServicesTest { + + private static final Integer PSM_ID = 4001; + + @Mock + private CalltypeRepo calltypeRepo; + + @InjectMocks + private CalltypeServiceImpl calltypeService; + + @Mock + private V_ShowsubcategoryRepo v_ShowsubcategoryRepo; + + @Mock + private CategoryRepo categoryRepo; + + @Mock + private SubCategoryRepo subCategoryRepo; + + @InjectMocks + private CategoryMasterImpl categoryService; + + @Mock + private DrugGroupRepo drugGroupRepo; + + @Mock + private DrugMasterRepo drugMasterRepo; + + @Mock + private DrugMappingRepo drugMappingRepo; + + @InjectMocks + private DrugMasterImpl drugService; + + @Mock + private InstuteDirectoryRepo instuteDirectoryRepo; + + @InjectMocks + private InstuteDirectoryServiceImpl directoryService; + + @Mock + private M_FeedbacknatureRepo m_FeedbacknatureRepo; + + @InjectMocks + private M_FeedbacknatureImpl feedbackNatureService; + + @Mock + private M_FeedbacktypeRepo m_FeedbacktypeRepo; + + @InjectMocks + private M_FeedbacktypeImpl feedbackTypeService; + + @Mock + private M_InstitutedirectorymappingRepo m_InstitutedirectorymappingRepo; + + @InjectMocks + private M_InstitutedirectorymappingImpl directoryMappingService; + + @Mock + private M_InstitutesubdirectoryRepo m_InstitutesubdirectoryRepo; + + @InjectMocks + private M_InstitutesubdirectoryImpl subDirectoryService; + + @Mock + private M_InstitutionRepo m_InstitutionRepo; + + @InjectMocks + private M_InstitutionImpl institutionService; + + @Mock + private M_InstitutiontypeRepo m_InstitutiontypeRepo; + + @InjectMocks + private M_InstitutiontypeImpl instituteTypeService; + + @Mock + private M_ServiceMasterRepo mservicemasteRepo; + + @InjectMocks + private M_ServiceMasterImpl serviceMasterService; + + @Mock + private M_SeverityRepo m_ServerityRepo; + + @InjectMocks + private M_SeverityImpl severityService; + + @Mock + private M_SubservicemasterPArepo m_SubservicemasterPArepo; + + @Mock + private SubserviceMasterRepo subserviceMasterRepo; + + @InjectMocks + private SubserviceImpl subServiceService; + + @Mock + private V_ShowprovideradminRepo v_ShowprovideradminRepo; + + @Mock + private M_UserservicerolemappingForRoleRepo m_UserservicerolemappingForRoleRepo; + + @Mock + private IemrServiceRepository1 iemrServiceRepository1; + + @Mock + private M_ProviderServiceMappingRepo m_ProviderServiceMappingRepo; + + @InjectMocks + private ServiceProvider_ServiceImpl providerService; + + @Nested + @DisplayName("CalltypeServiceImpl") + class CallTypeServiceTests { + + @Test + @DisplayName("should hand each call to its own repository query") + void callType_shouldReachTheirOwnQuery() { + M_Calltype callType = new M_Calltype(); + ArrayList stored = new ArrayList<>(List.of(callType)); + when(calltypeRepo.saveAll(anyList())).thenReturn(stored); + when(calltypeRepo.updateCallType(51)).thenReturn(callType); + when(calltypeRepo.save(callType)).thenReturn(callType); + when(calltypeRepo.getCalltypeData(PSM_ID)).thenReturn(stored); + + assertSame(stored, calltypeService.saveCallList(new ArrayList<>())); + assertSame(stored, calltypeService.createCalltype(new ArrayList<>())); + assertSame(callType, calltypeService.updateCallType(51)); + assertSame(callType, calltypeService.saveupdatedData(callType)); + assertSame(stored, calltypeService.getCalltypeData(PSM_ID)); + } + } + + @Nested + @DisplayName("CategoryMasterImpl") + class CategoryServiceTests { + + @Test + @DisplayName("getCategoryId should store the category and answer the id it was given") + void getCategoryId_shouldStoreAndAnswerId() { + M_Category category = new M_Category(); + M_Category stored = new M_Category(); + stored.setCategoryID(81); + when(categoryRepo.save(category)).thenReturn(stored); + + assertEquals(81, categoryService.getCategoryId(category)); + } + + @Test + @DisplayName("the sub-category calls should each reach their own repository query") + void subCategoryCalls_shouldReachTheirOwnQuery() { + M_Subcategory subCategory = new M_Subcategory(); + ArrayList stored = new ArrayList<>(List.of(subCategory)); + ArrayList views = new ArrayList<>(List.of(new V_Showsubcategory())); + when(subCategoryRepo.saveAll(anyList())).thenReturn(stored); + when(subCategoryRepo.getCategory()).thenReturn(stored); + when(subCategoryRepo.getCategory(81)).thenReturn(stored); + when(subCategoryRepo.getSubCategory(91)).thenReturn(subCategory); + when(subCategoryRepo.save(subCategory)).thenReturn(subCategory); + when(v_ShowsubcategoryRepo.getSubCategory1(91)).thenReturn(views); + when(v_ShowsubcategoryRepo.getCategoryByMapIDAndSubServiceID(PSM_ID, 61)).thenReturn(views); + + assertSame(stored, categoryService.saveSubCatData(new ArrayList<>())); + assertSame(stored, categoryService.createSubCategory(new ArrayList<>())); + assertSame(stored, categoryService.getCategory()); + assertSame(stored, categoryService.getCategory(81)); + assertSame(subCategory, categoryService.getSubCategory(91)); + assertSame(subCategory, categoryService.updateSubCatData(subCategory)); + assertSame(views, categoryService.getSubCategory1(91)); + assertSame(views, categoryService.getCategoryByMapIDAndSubServiceID(PSM_ID, 61)); + } + + @Test + @DisplayName("the category calls should each reach their own repository query") + void categoryCalls_shouldReachTheirOwnQuery() { + M_Category category = new M_Category(); + ArrayList stored = new ArrayList<>(List.of(category)); + when(categoryRepo.saveAll(anyList())).thenReturn(stored); + when(categoryRepo.getAllCategory(61, PSM_ID)).thenReturn(stored); + when(categoryRepo.getAllCategory1(PSM_ID)).thenReturn(stored); + when(categoryRepo.getCatData(81)).thenReturn(category); + when(categoryRepo.save(category)).thenReturn(category); + when(categoryRepo.updateCategory(81, 21)).thenReturn(1); + when(categoryRepo.findByProviderServiceMapIDAndFeedbackNatureIDOrderByCategoryNameAsc(PSM_ID, null)) + .thenReturn(stored); + + assertSame(stored, categoryService.createcat(new ArrayList<>())); + assertSame(stored, categoryService.getAllCategory(61, PSM_ID)); + assertSame(stored, categoryService.getAllCategory1(PSM_ID)); + assertSame(category, categoryService.getcatdatabycatId(81)); + assertSame(category, categoryService.deletedata(category)); + assertEquals(1, categoryService.updateCategory(81, 21)); + assertSame(stored, categoryService.getUpmappedCategory(PSM_ID)); + } + + @Test + @DisplayName("getAllCategory should rebuild one category per row the query answers") + void getAllCategory_shouldRebuildEachRow() { + when(categoryRepo.getAllCategory(PSM_ID)) + .thenReturn(List.of(new Object[] { 81, "Medical", 61, "Counselling", PSM_ID })); + + ArrayList categories = categoryService.getAllCategory(PSM_ID); + + assertEquals(1, categories.size()); + assertEquals("Medical", categories.get(0).getCategoryName()); + } + + @Test + @DisplayName("getAllCategorywithFeedbackNatureID should rebuild one category per row the query answers") + void getAllCategoryWithFeedbackNature_shouldRebuildEachRow() { + when(categoryRepo.getAllCategorywithfeedbackNatureID(PSM_ID, 21)) + .thenReturn(List.of(new Object[] { 81, "Medical", 61, "Counselling", PSM_ID })); + + assertEquals(1, categoryService.getAllCategorywithFeedbackNatureID(PSM_ID, 21).size()); + } + } + + @Nested + @DisplayName("DrugMasterImpl") + class DrugServiceTests { + + @Test + @DisplayName("getAllDrugData should read the live catalogue when the caller excludes retired drugs") + void getAllDrugData_shouldReadLiveCatalogue() { + when(drugMasterRepo.getValidDrugData("77")) + .thenReturn(List.of(new Object[] { 101, "Paracetamol", "Antipyretic", "OTC", + Boolean.FALSE, (short) 77 })); + + ArrayList drugs = drugService.getAllDrugData(101, (short) 77, Boolean.FALSE); + + assertEquals(1, drugs.size()); + assertEquals("Paracetamol", drugs.get(0).getDrugName()); + verify(drugMasterRepo, org.mockito.Mockito.never()).getAllDrugData(anyString(), anyString()); + } + + @Test + @DisplayName("getAllDrugData should read the whole catalogue when retired drugs are wanted too") + void getAllDrugData_shouldReadWholeCatalogue() { + when(drugMasterRepo.getAllDrugData("101", "77")).thenReturn(new ArrayList<>()); + + drugService.getAllDrugData(101, (short) 77, Boolean.TRUE); + + verify(drugMasterRepo).getAllDrugData("101", "77"); + } + + @Test + @DisplayName("getAllDrugData should ask for the whole catalogue when the caller names nothing") + void getAllDrugData_shouldAskForWholeCatalogueWithoutFilters() { + when(drugMasterRepo.getAllDrugData("", "")).thenReturn(new ArrayList<>()); + + drugService.getAllDrugData(null, null, null); + + verify(drugMasterRepo).getAllDrugData("", ""); + } + + @Test + @DisplayName("getAllDrugGroups should read the live groups when the caller excludes retired ones") + void getAllDrugGroups_shouldReadLiveGroups() { + when(drugGroupRepo.getValidDrugGroups("77")) + .thenReturn(List.of(new Object[] { 201, "Analgesics", "Pain relief", Boolean.FALSE, + (short) 77 })); + + assertEquals(1, drugService.getAllDrugGroups(201, (short) 77, Boolean.FALSE).size()); + } + + @Test + @DisplayName("getAllDrugGroups should read every group when retired ones are wanted too") + void getAllDrugGroups_shouldReadEveryGroup() { + when(drugGroupRepo.getAllDrugGroups("201", "77")).thenReturn(new ArrayList<>()); + + drugService.getAllDrugGroups(201, (short) 77, Boolean.TRUE); + + verify(drugGroupRepo).getAllDrugGroups("201", "77"); + } + + @Test + @DisplayName("getAllDrugGroupMappings should rebuild one mapping per row the query answers") + void getAllDrugGroupMappings_shouldRebuildEachRow() { + when(drugMappingRepo.getAllDrugGroupMappings("", 77, 3)) + .thenReturn(List.of(new Object[] { 301, 101, "Paracetamol", 201, "Analgesics", "OTC", + Boolean.FALSE, 77, PSM_ID, "N", Boolean.FALSE })); + + assertEquals(1, drugService.getAllDrugGroupMappings(null, 77, 3).size()); + } + + @Test + @DisplayName("the drug writes should each reach their own repository") + void drugWrites_shouldReachTheirOwnRepository() { + M_104druggroup group = new M_104druggroup(); + group.setDrugGroupID(201); + M_104drugmaster drug = new M_104drugmaster(); + M_104drugmapping mapping = new M_104drugmapping(); + ArrayList groups = new ArrayList<>(List.of(group)); + ArrayList drugs = new ArrayList<>(List.of(drug)); + ArrayList mappings = new ArrayList<>(List.of(mapping)); + when(drugGroupRepo.save(group)).thenReturn(group); + when(drugGroupRepo.saveAll(anyList())).thenReturn(groups); + when(drugGroupRepo.getDrugGroupById(201)).thenReturn(group); + when(drugMasterRepo.saveAll(anyList())).thenReturn(drugs); + when(drugMasterRepo.save(drug)).thenReturn(drug); + when(drugMasterRepo.getDrugDataById(101)).thenReturn(drug); + when(drugMappingRepo.saveAll(anyList())).thenReturn(mappings); + when(drugMappingRepo.save(mapping)).thenReturn(mapping); + when(drugMappingRepo.getDrugMappingById(301)).thenReturn(mapping); + + assertEquals(201, drugService.getDrugGrupId(group)); + assertSame(groups, drugService.saveDrugGroup(new ArrayList<>())); + assertSame(group, drugService.getDrugGroupById(201)); + assertSame(group, drugService.saveUpdatedDrugGroup(group)); + assertSame(drugs, drugService.saveDrugData(new ArrayList<>())); + assertSame(drug, drugService.getDrugDataById(101)); + assertSame(drug, drugService.saveUpdatedData(drug)); + assertSame(mappings, drugService.mapDrugWithGroup(new ArrayList<>())); + assertSame(mapping, drugService.getDrugMappingsById(301)); + assertSame(mapping, drugService.saveUpdatedDrugMapping(mapping)); + } + + @Test + @DisplayName("the status updates should each reach their own repository query") + void statusUpdates_shouldReachTheirOwnQuery() { + M_104druggroup group = new M_104druggroup(); + group.setDrugGroupID(201); + group.setDeleted(Boolean.TRUE); + group.setModifiedBy("admin"); + M_104drugmaster drug = new M_104drugmaster(); + drug.setDrugID(101); + drug.setDeleted(Boolean.TRUE); + drug.setModifiedBy("admin"); + M_104drugmapping mapping = new M_104drugmapping(); + mapping.setDrugMapID(301); + mapping.setDeleted(Boolean.TRUE); + mapping.setModifiedBy("admin"); + when(drugGroupRepo.updateStatus(201, Boolean.TRUE, "admin")).thenReturn(1); + when(drugMasterRepo.updateStatus(101, Boolean.TRUE, "admin")).thenReturn(1); + when(drugMappingRepo.updateStatus(301, Boolean.TRUE, "admin")).thenReturn(1); + + assertEquals(1, drugService.updateDrugGroupStatus(group)); + assertEquals(1, drugService.updateDrugStatus(drug)); + assertEquals(1, drugService.updateDrugMappingStatus(mapping)); + } + } + + @Nested + @DisplayName("Institute services") + class InstituteServiceTests { + + @Test + @DisplayName("the directory calls should each reach their own repository query") + void directoryCalls_shouldReachTheirOwnQuery() { + M_Institutedirectory directory = new M_Institutedirectory(); + ArrayList stored = new ArrayList<>(List.of(directory)); + when(instuteDirectoryRepo.saveAll(anyList())).thenReturn(stored); + when(instuteDirectoryRepo.getInstuteDirectory(PSM_ID)).thenReturn(stored); + when(instuteDirectoryRepo.editInstuteDirectory(11)).thenReturn(directory); + when(instuteDirectoryRepo.save(directory)).thenReturn(directory); + + assertSame(stored, directoryService.createInstuteDirectory(new ArrayList<>())); + assertSame(stored, directoryService.getInstuteDirectory(PSM_ID)); + assertSame(directory, directoryService.editInstuteDirectory(11)); + assertSame(directory, directoryService.editdata(directory)); + } + + @Test + @DisplayName("the sub-directory calls should each reach their own repository query") + void subDirectoryCalls_shouldReachTheirOwnQuery() { + M_Institutesubdirectory subDirectory = new M_Institutesubdirectory(); + ArrayList stored = new ArrayList<>(List.of(subDirectory)); + when(m_InstitutesubdirectoryRepo.getInstutesubDirectory(11, PSM_ID)).thenReturn(stored); + when(m_InstitutesubdirectoryRepo.saveAll(anyList())).thenReturn(stored); + when(m_InstitutesubdirectoryRepo.editInstutesubDirectory(41)).thenReturn(subDirectory); + when(m_InstitutesubdirectoryRepo.save(subDirectory)).thenReturn(subDirectory); + + assertSame(stored, subDirectoryService.getInstutesubDirectory(11, PSM_ID)); + assertSame(stored, subDirectoryService.CreateInstutesubDirectory(new ArrayList<>())); + assertSame(subDirectory, subDirectoryService.editInstutesubDirectory(41)); + assertSame(subDirectory, subDirectoryService.saveEditedData(subDirectory)); + } + + @Test + @DisplayName("getInstituteDirectoryData should skip a row the query could not fill") + void getInstituteDirectoryData_shouldSkipUnfillableRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { 51, 31, 11, 41, PSM_ID, Boolean.FALSE, "admin", "District Hospital", + "Hospitals", "Government" }); + when(m_InstitutedirectorymappingRepo.getMappingData(41)).thenReturn(rows); + + assertEquals(1, directoryMappingService.getInstituteDirectoryData(41).size()); + } + + @Test + @DisplayName("the directory mapping writes should each reach their own repository query") + void directoryMappingWrites_shouldReachTheirOwnQuery() { + M_Institutedirectorymapping mapping = new M_Institutedirectorymapping(); + ArrayList stored = new ArrayList<>(List.of(mapping)); + when(m_InstitutedirectorymappingRepo.saveAll(anyList())).thenReturn(stored); + when(m_InstitutedirectorymappingRepo.getdata(51)).thenReturn(mapping); + when(m_InstitutedirectorymappingRepo.save(mapping)).thenReturn(mapping); + + assertSame(stored, directoryMappingService.createInstituteDirectoryData(new ArrayList<>())); + assertSame(mapping, directoryMappingService.deleteInstituteDirectoryData(51)); + assertSame(mapping, directoryMappingService.setdeletedData(mapping)); + } + + @Test + @DisplayName("getInstution should narrow by block only when a block is named") + void getInstution_shouldNarrowByBlockOnlyWhenNamed() { + ArrayList stored = new ArrayList<>(); + when(m_InstitutionRepo.getInstution(PSM_ID, 29, 301)).thenReturn(stored); + when(m_InstitutionRepo.getInstution(PSM_ID, 29, 301, 401)).thenReturn(stored); + + institutionService.getInstution(PSM_ID, 29, 301, null); + institutionService.getInstution(PSM_ID, 29, 301, 401); + + verify(m_InstitutionRepo).getInstution(PSM_ID, 29, 301); + verify(m_InstitutionRepo).getInstution(PSM_ID, 29, 301, 401); + } + + @Test + @DisplayName("getInstutionByVillage should pick the query that matches what the caller named") + void getInstutionByVillage_shouldPickMatchingQuery() { + ArrayList stored = new ArrayList<>(); + when(m_InstitutionRepo.getInstution(PSM_ID, 29, 301)).thenReturn(stored); + when(m_InstitutionRepo.getInstutionByBlock(PSM_ID, 29, 301, 401)).thenReturn(stored); + when(m_InstitutionRepo.getInstutionByVillage(PSM_ID, 29, 301, 501)).thenReturn(stored); + when(m_InstitutionRepo.getInstutionByBlockAndVillage(PSM_ID, 29, 301, 401, 501)).thenReturn(stored); + + institutionService.getInstutionByVillage(PSM_ID, 29, 301, null, null); + institutionService.getInstutionByVillage(PSM_ID, 29, 301, 401, null); + institutionService.getInstutionByVillage(PSM_ID, 29, 301, null, 501); + institutionService.getInstutionByVillage(PSM_ID, 29, 301, 401, 501); + + verify(m_InstitutionRepo).getInstution(PSM_ID, 29, 301); + verify(m_InstitutionRepo).getInstutionByBlock(PSM_ID, 29, 301, 401); + verify(m_InstitutionRepo).getInstutionByVillage(PSM_ID, 29, 301, 501); + verify(m_InstitutionRepo).getInstutionByBlockAndVillage(PSM_ID, 29, 301, 401, 501); + } + + @Test + @DisplayName("the institution writes should each reach their own repository query") + void institutionWrites_shouldReachTheirOwnQuery() { + M_Institution institution = new M_Institution(); + ArrayList stored = new ArrayList<>(List.of(institution)); + when(m_InstitutionRepo.saveAll(anyList())).thenReturn(stored); + when(m_InstitutionRepo.geteditedData(31)).thenReturn(institution); + when(m_InstitutionRepo.save(institution)).thenReturn(institution); + + assertSame(stored, institutionService.createInstution(new ArrayList<>())); + assertSame(stored, institutionService.createInstutionByVillage(new ArrayList<>())); + assertSame(institution, institutionService.editInstution(31)); + assertSame(institution, institutionService.saveEditData(institution)); + } + + @Test + @DisplayName("createInstitutionByFile should report a run that stored rows") + void createInstitutionByFile_shouldReportStoredRows() { + ArrayList answer = new ArrayList<>(); + answer.add(new Object[] { 12, 0 }); + when(m_InstitutionRepo.institutionByFile(anyString(), anyString(), anyInt(), anyInt())) + .thenReturn(answer); + + assertEquals("Data Saved Successfully", institutionService.createInstitutionByFile(uploadRequest())); + } + + @Test + @DisplayName("createInstitutionByFile should report a file whose rows are already on record") + void createInstitutionByFile_shouldReportDuplicateRows() { + ArrayList answer = new ArrayList<>(); + answer.add(new Object[] { 0, 12 }); + when(m_InstitutionRepo.institutionByFile(anyString(), anyString(), anyInt(), anyInt())) + .thenReturn(answer); + + assertEquals("Data is already present", institutionService.createInstitutionByFile(uploadRequest())); + } + + @Test + @DisplayName("createInstitutionByFile should report a file the database could not read") + void createInstitutionByFile_shouldReportUnusableFile() { + ArrayList answer = new ArrayList<>(); + answer.add(new Object[] { -1, 0 }); + when(m_InstitutionRepo.institutionByFile(anyString(), anyString(), anyInt(), anyInt())) + .thenReturn(answer); + + assertEquals("The Data in file is not appropriate", + institutionService.createInstitutionByFile(uploadRequest())); + } + + @Test + @DisplayName("createInstitutionByFile should answer nothing when the database reports nothing") + void createInstitutionByFile_shouldAnswerNothingForEmptyReport() { + when(m_InstitutionRepo.institutionByFile(anyString(), anyString(), anyInt(), anyInt())) + .thenReturn(new ArrayList<>()); + + assertNull(institutionService.createInstitutionByFile(uploadRequest())); + } + + private JsonObject uploadRequest() { + JsonObject request = new JsonObject(); + request.addProperty("createdBy", "admin"); + request.addProperty("userID", 3117); + request.addProperty("serviceProviderID", 77); + return request; + } + + @Test + @DisplayName("getInstuteTypeByDist should pick the query that matches what the caller named") + void getInstuteTypeByDist_shouldPickMatchingQuery() { + ArrayList stored = new ArrayList<>(); + when(m_InstitutiontypeRepo.getInstuteTypeByDist(PSM_ID, 301)).thenReturn(stored); + when(m_InstitutiontypeRepo.getInstutionTypeByBlock(PSM_ID, 301, 401)).thenReturn(stored); + when(m_InstitutiontypeRepo.getInstutionTypeByVillage(PSM_ID, 301, 501)).thenReturn(stored); + when(m_InstitutiontypeRepo.getInstutionByBlockAndVillage(PSM_ID, 301, 401, 501)).thenReturn(stored); + + instituteTypeService.getInstuteTypeByDist(PSM_ID, 301, null, null); + instituteTypeService.getInstuteTypeByDist(PSM_ID, 301, 401, null); + instituteTypeService.getInstuteTypeByDist(PSM_ID, 301, null, 501); + instituteTypeService.getInstuteTypeByDist(PSM_ID, 301, 401, 501); + + verify(m_InstitutiontypeRepo).getInstuteTypeByDist(PSM_ID, 301); + verify(m_InstitutiontypeRepo).getInstutionTypeByBlock(PSM_ID, 301, 401); + verify(m_InstitutiontypeRepo).getInstutionTypeByVillage(PSM_ID, 301, 501); + verify(m_InstitutiontypeRepo).getInstutionByBlockAndVillage(PSM_ID, 301, 401, 501); + } + + @Test + @DisplayName("the institute type writes should each reach their own repository query") + void instituteTypeWrites_shouldReachTheirOwnQuery() { + M_Institutiontype type = new M_Institutiontype(); + ArrayList stored = new ArrayList<>(List.of(type)); + when(m_InstitutiontypeRepo.saveAll(anyList())).thenReturn(stored); + when(m_InstitutiontypeRepo.getInstuteType(PSM_ID)).thenReturn(stored); + when(m_InstitutiontypeRepo.editdata(21)).thenReturn(type); + when(m_InstitutiontypeRepo.save(type)).thenReturn(type); + + assertSame(stored, instituteTypeService.createInstuteType(new ArrayList<>())); + assertSame(stored, instituteTypeService.createInstuteTypeByDist(new ArrayList<>())); + assertSame(stored, instituteTypeService.getInstuteType(PSM_ID)); + assertSame(type, instituteTypeService.editInstuteType(21)); + assertSame(type, instituteTypeService.saveEditdata(type)); + } + } + + @Nested + @DisplayName("Feedback, severity and sub-service services") + class RemainingServiceTests { + + @Test + @DisplayName("the feedback nature calls should each reach their own repository query") + void feedbackNatureCalls_shouldReachTheirOwnQuery() { + M_Feedbacknature nature = new M_Feedbacknature(); + ArrayList stored = new ArrayList<>(List.of(nature)); + when(m_FeedbacknatureRepo.getInstuteType(41)).thenReturn(stored); + when(m_FeedbacknatureRepo.saveAll(anyList())).thenReturn(stored); + when(m_FeedbacknatureRepo.editFeedbackNatureType(21)).thenReturn(nature); + when(m_FeedbacknatureRepo.save(nature)).thenReturn(nature); + + assertSame(stored, feedbackNatureService.getFeedbackNatureType(41)); + assertSame(stored, feedbackNatureService.createFeedbackNatueType(new ArrayList<>())); + assertSame(nature, feedbackNatureService.editFeedbackNatureType(21)); + assertSame(nature, feedbackNatureService.saveEditedData(nature)); + } + + @Test + @DisplayName("the feedback type calls should each reach their own repository query") + void feedbackTypeCalls_shouldReachTheirOwnQuery() { + M_Feedbacktype type = new M_Feedbacktype(); + ArrayList stored = new ArrayList<>(List.of(type)); + when(m_FeedbacktypeRepo.getAllFeedbackType(PSM_ID)).thenReturn(stored); + when(m_FeedbacktypeRepo.saveAll(anyList())).thenReturn(stored); + when(m_FeedbacktypeRepo.deleteFeedback(41)).thenReturn(type); + when(m_FeedbacktypeRepo.save(type)).thenReturn(type); + + assertSame(stored, feedbackTypeService.getFeedbackt(PSM_ID)); + assertSame(stored, feedbackTypeService.saveFeedbackType(new ArrayList<>())); + assertSame(type, feedbackTypeService.getDataByServId(41)); + assertSame(type, feedbackTypeService.deletedataser(type)); + } + + @Test + @DisplayName("the severity calls should each reach their own repository query") + void severityCalls_shouldReachTheirOwnQuery() { + M_Severity severity = new M_Severity(); + ArrayList stored = new ArrayList<>(List.of(severity)); + when(m_ServerityRepo.getAllServerity(PSM_ID)).thenReturn(stored); + when(m_ServerityRepo.saveAll(anyList())).thenReturn(stored); + when(m_ServerityRepo.editServerity(31)).thenReturn(severity); + when(m_ServerityRepo.save(severity)).thenReturn(severity); + + assertSame(stored, severityService.getServerity(PSM_ID)); + assertSame(stored, severityService.saveServerity(new ArrayList<>())); + assertSame(severity, severityService.getDataByServId(31)); + assertSame(severity, severityService.deletedataser(severity)); + } + + @Test + @DisplayName("the sub-service calls should each reach their own repository query") + void subServiceCalls_shouldReachTheirOwnQuery() { + M_Subservice subService = new M_Subservice(); + ArrayList stored = new ArrayList<>(List.of(subService)); + ArrayList masters = new ArrayList<>(List.of(new M_SubservicemasterPA())); + when(subserviceMasterRepo.saveAll(anyList())).thenReturn(stored); + when(subserviceMasterRepo.getsubServiceName(PSM_ID)).thenReturn(stored); + when(subserviceMasterRepo.getsubServiceNameById(61)).thenReturn(subService); + when(subserviceMasterRepo.save(subService)).thenReturn(subService); + when(m_SubservicemasterPArepo.getServiceNameByServiceID(3)).thenReturn(masters); + + assertSame(stored, subServiceService.saveSubList(new ArrayList<>())); + assertSame(stored, subServiceService.getsubServiceName(PSM_ID)); + assertSame(subService, subServiceService.getsubServiceNameById(61)); + assertSame(subService, subServiceService.saveupdatedData(subService)); + assertSame(masters, subServiceService.getServiceNameByServiceID(3)); + } + + @Test + @DisplayName("getAllServiceLine should hand back what the repository holds") + void getAllServiceLine_shouldHandBackRepositoryContents() { + List stored = List.of(new M_ServiceMaster()); + when(mservicemasteRepo.getAllServiceline()).thenReturn(stored); + + assertSame(stored, serviceMasterService.getAllServiceLine()); + } + } + + @Nested + @DisplayName("ServiceProvider_ServiceImpl") + class ProviderServiceTests { + + @Test + @DisplayName("createProvider should answer the id of the provider it stored") + void createProvider_shouldAnswerStoredId() { + ServiceProvider_Model stored = new ServiceProvider_Model(); + stored.setServiceProviderId(77); + when(iemrServiceRepository1.saveAll(any(Set.class))) + .thenReturn(new ArrayList<>(List.of(stored))); + + assertEquals(77, providerService.createProvider(Set.of(new ServiceProvider_Model()))); + } + + @Test + @DisplayName("createProvider1 should refuse an empty batch rather than store nothing quietly") + void createProvider1_shouldRefuseEmptyBatch() { + assertThrows(DataNotFound.class, () -> providerService.createProvider1(new ArrayList<>())); + } + + @Test + @DisplayName("createProvider1 should refuse a batch the repository stored nothing from") + void createProvider1_shouldRefuseBatchThatStoredNothing() { + when(iemrServiceRepository1.saveAll(anyList())).thenReturn(new ArrayList<>()); + + assertThrows(DataNotFound.class, + () -> providerService.createProvider1(List.of(new ServiceProvider_Model()))); + } + + @Test + @DisplayName("createProvider1 should answer what the repository stored") + void createProvider1_shouldAnswerStoredProviders() { + ArrayList stored = new ArrayList<>(List.of(new ServiceProvider_Model())); + when(iemrServiceRepository1.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, providerService.createProvider1(List.of(new ServiceProvider_Model()))); + } + + @Test + @DisplayName("the provider lookups should each reach their own repository query") + void providerLookups_shouldReachTheirOwnQuery() { + ServiceProvider_Model provider = new ServiceProvider_Model(); + ArrayList providers = new ArrayList<>(List.of(provider)); + M_ProviderServiceMapping mapping = new M_ProviderServiceMapping(); + M_UserservicerolemappingForRole roleMapping = new M_UserservicerolemappingForRole(); + ArrayList roleMappings = new ArrayList<>(List.of(roleMapping)); + ArrayList admins = new ArrayList<>(List.of(new V_Showprovideradmin())); + when(iemrServiceRepository1.getProviderName("Piramal Swasthya")).thenReturn("Piramal Swasthya"); + when(iemrServiceRepository1.getAllProviderName()).thenReturn(providers); + when(iemrServiceRepository1.getProviderData(77)).thenReturn(provider); + when(iemrServiceRepository1.save(provider)).thenReturn(provider); + when(iemrServiceRepository1.saveAll(anyList())).thenReturn(providers); + when(m_ProviderServiceMappingRepo.getPSMID(PSM_ID)).thenReturn(mapping); + when(m_ProviderServiceMappingRepo.saveAll(any(Set.class))).thenReturn(List.of(mapping)); + when(m_UserservicerolemappingForRoleRepo.saveAll(anyList())).thenReturn(roleMappings); + when(m_UserservicerolemappingForRoleRepo.findByUSRMappingID(9001)).thenReturn(roleMapping); + when(m_UserservicerolemappingForRoleRepo.save(roleMapping)).thenReturn(roleMapping); + when(v_ShowprovideradminRepo.getAllProviderAdmin()).thenReturn(admins); + + assertEquals("Piramal Swasthya", providerService.getProviderName("Piramal Swasthya")); + assertSame(providers, providerService.getAllProviderName()); + assertSame(provider, providerService.getProviderData(77)); + assertSame(provider, providerService.upDateProviderDetails(provider)); + assertSame(providers, providerService.createProvider(List.of(provider))); + assertSame(mapping, providerService.getProviderserviceMapId(PSM_ID)); + assertEquals(1, providerService.mapProviderStateService(Set.of(mapping)).size()); + assertSame(roleMappings, providerService.AddUserRole(new ArrayList<>())); + assertSame(roleMapping, providerService.getPADataForEdit(9001)); + assertSame(roleMapping, providerService.insertEditedData(roleMapping)); + assertSame(admins, providerService.getProviderAdmins()); + } + } +} diff --git a/src/test/java/com/iemr/admin/service/questionnaire/QuestionnaireServiceImplTest.java b/src/test/java/com/iemr/admin/service/questionnaire/QuestionnaireServiceImplTest.java new file mode 100644 index 0000000..683c443 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/questionnaire/QuestionnaireServiceImplTest.java @@ -0,0 +1,233 @@ +/* +* 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.admin.service.questionnaire; + +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.admin.data.questionnaire.Questionnaire; +import com.iemr.admin.data.questionnaire.QuestionnaireValues; +import com.iemr.admin.repo.questionnaire.QuestionnaireRepository; +import com.iemr.admin.repo.questionnaire.QuestionnaireValuesRepository; + +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.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.anyString; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The questionnaire service keeps the feedback questions a provider asks, the + * options each question offers, and the order the questions are asked in. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("QuestionnaireServiceImpl Test Suite") +class QuestionnaireServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer QUESTION_ID = 501; + + @Mock + private QuestionnaireRepository questionnaireRepository; + + @Mock + private QuestionnaireValuesRepository questionnaireValuesRepository; + + @InjectMocks + private QuestionnaireServiceImpl service; + + private static final String SAVE_REQUEST = "[{\"questionnaireDetail\":{\"question\":\"Was the visit useful?\"," + + "\"questionRank\":1,\"questionWeightage\":5,\"answerType\":\"Radio\",\"providerServiceMapID\":4001," + + "\"createdBy\":\"admin\",\"questionOptions\":[{\"option\":\"Yes\",\"optionWeightage\":5}]}}]"; + + private static Questionnaire stored() { + Questionnaire question = new Questionnaire(); + question.setQuestionID(QUESTION_ID); + question.setQuestion("Was the visit useful?"); + question.setQuestionRank(1); + question.setProviderServiceMapID(PSM_ID); + question.setCreatedBy("admin"); + QuestionnaireValues option = new QuestionnaireValues(); + option.setOption("Yes"); + question.setQuestionOptions(new ArrayList<>(List.of(option))); + return question; + } + + @Test + @DisplayName("SaveQuestionnaire should store the question and stamp its options with the question it belongs to") + void save_shouldStoreQuestionAndStampOptions() throws Exception { + when(questionnaireRepository.findQuestionWithRank(PSM_ID, 1)).thenReturn(0); + when(questionnaireRepository.save(any(Questionnaire.class))).thenReturn(stored()); + + assertEquals("Questionnaire Data Saved Successfully", service.SaveQuestionnaire(SAVE_REQUEST)); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(questionnaireValuesRepository).saveAll(captor.capture()); + assertEquals(QUESTION_ID, captor.getValue().get(0).getQuestionID()); + assertEquals(PSM_ID, captor.getValue().get(0).getProviderServiceMapID()); + assertEquals("admin", captor.getValue().get(0).getCreatedBy()); + } + + @Test + @DisplayName("SaveQuestionnaire should push the questions below down when the new one takes an occupied rank") + void save_shouldPushLowerQuestionsDown() throws Exception { + when(questionnaireRepository.findQuestionWithRank(PSM_ID, 1)).thenReturn(1); + when(questionnaireRepository.save(any(Questionnaire.class))).thenReturn(stored()); + + service.SaveQuestionnaire(SAVE_REQUEST); + + verify(questionnaireRepository).updateRankToNext(PSM_ID, 1); + } + + @Test + @DisplayName("SaveQuestionnaire should leave the order alone when the new question's rank is free") + void save_shouldLeaveOrderAloneForFreeRank() throws Exception { + when(questionnaireRepository.findQuestionWithRank(PSM_ID, 1)).thenReturn(0); + when(questionnaireRepository.save(any(Questionnaire.class))).thenReturn(stored()); + + service.SaveQuestionnaire(SAVE_REQUEST); + + verify(questionnaireRepository, never()).updateRankToNext(anyInt(), anyInt()); + } + + @Test + @DisplayName("SaveQuestionnaire should give up when the question cannot be stored") + void save_shouldGiveUpWhenStorageFails() { + when(questionnaireRepository.save(any(Questionnaire.class))) + .thenThrow(new RuntimeException("row is locked")); + + assertThrows(RuntimeException.class, () -> service.SaveQuestionnaire(SAVE_REQUEST)); + } + + @Test + @DisplayName("getQuestionnaireList should answer the multiple choice and free text questions together") + void getList_shouldAnswerBothKindsOfQuestion() { + Questionnaire freeText = new Questionnaire(); + freeText.setQuestion("Anything else?"); + when(questionnaireRepository.findAllQuestions(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(stored()))); + when(questionnaireRepository.findAllQuestionsFreeText(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(freeText))); + + String answered = service.getQuestionnaireList("{\"providerServiceMapID\":4001}"); + + assertTrue(answered.contains("Was the visit useful?"), answered); + assertTrue(answered.contains("Anything else?"), answered); + } + + @Test + @DisplayName("getQuestionnaireList should answer an empty list when the provider asks nothing") + void getList_shouldAnswerEmptyListForProviderWithNoQuestions() { + when(questionnaireRepository.findAllQuestions(PSM_ID)).thenReturn(new ArrayList<>()); + when(questionnaireRepository.findAllQuestionsFreeText(PSM_ID)).thenReturn(new ArrayList<>()); + + assertEquals("[]", service.getQuestionnaireList("{\"providerServiceMapID\":4001}")); + } + + @Test + @DisplayName("deleteQuestionnaire should retire the question, its options and close the gap in the order") + void delete_shouldRetireQuestionAndCloseGap() { + when(questionnaireRepository.deleteQuestion(PSM_ID, QUESTION_ID, Boolean.TRUE)).thenReturn(1); + when(questionnaireRepository.deleteOptions(PSM_ID, QUESTION_ID, Boolean.TRUE)).thenReturn(1); + + assertEquals("Questionnaire Deleted Successfully", service.deleteQuestionnaire( + "{\"providerServiceMapID\":4001,\"questionID\":501,\"questionRank\":1,\"deleted\":true}")); + verify(questionnaireRepository).updateRankToPrevious(PSM_ID, 1); + } + + @Test + @DisplayName("deleteQuestionnaire should answer nothing and leave the order alone when no question was retired") + void delete_shouldAnswerNothingWhenNoQuestionRetired() { + when(questionnaireRepository.deleteQuestion(anyInt(), anyInt(), anyBoolean())).thenReturn(0); + when(questionnaireRepository.deleteOptions(anyInt(), anyInt(), anyBoolean())).thenReturn(0); + + assertNull(service.deleteQuestionnaire( + "{\"providerServiceMapID\":4001,\"questionID\":-1,\"questionRank\":1,\"deleted\":true}")); + verify(questionnaireRepository, never()).updateRankToPrevious(anyInt(), anyInt()); + } + + @Test + @DisplayName("editQuestionnaire should change the question and the options that already exist") + void edit_shouldChangeQuestionAndExistingOptions() { + String request = "{\"questionnaireDetail\":{\"questionID\":501,\"question\":\"Was the visit helpful?\"," + + "\"questionWeightage\":6,\"answerType\":\"Radio\",\"providerServiceMapID\":4001," + + "\"modifiedBy\":\"supervisor\",\"questionOptions\":[{\"questionValuesID\":9001," + + "\"option\":\"Yes\",\"optionWeightage\":6,\"deleted\":false}]}}"; + + assertEquals("Questionnaire Updated Successfully", service.editQuestionnaire(request)); + + verify(questionnaireRepository).updateQuestion(QUESTION_ID, "Was the visit helpful?", 6, "Radio", PSM_ID, + "supervisor"); + verify(questionnaireRepository).updateAnswer(QUESTION_ID, 9001, "Yes", 6, "supervisor", Boolean.FALSE); + verify(questionnaireValuesRepository, never()).save(any(QuestionnaireValues.class)); + } + + @Test + @DisplayName("editQuestionnaire should add an option that has never been stored before") + void edit_shouldAddBrandNewOption() { + String request = "{\"questionnaireDetail\":{\"questionID\":501,\"question\":\"Was the visit helpful?\"," + + "\"questionWeightage\":6,\"answerType\":\"Radio\",\"providerServiceMapID\":4001," + + "\"createdBy\":\"admin\",\"modifiedBy\":\"supervisor\"," + + "\"questionOptions\":[{\"option\":\"Maybe\",\"optionWeightage\":3}]}}"; + + service.editQuestionnaire(request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(QuestionnaireValues.class); + verify(questionnaireValuesRepository).save(captor.capture()); + assertEquals("Maybe", captor.getValue().getOption()); + assertEquals(QUESTION_ID, captor.getValue().getQuestionID()); + assertEquals(PSM_ID, captor.getValue().getProviderServiceMapID()); + assertEquals("supervisor", captor.getValue().getModifiedBy()); + verify(questionnaireRepository, never()).updateAnswer(anyInt(), anyInt(), anyString(), anyInt(), anyString(), + anyBoolean()); + } + + @Test + @DisplayName("editQuestionnaire should give up when the change cannot be recorded") + void edit_shouldGiveUpWhenStorageFails() { + when(questionnaireRepository.updateQuestion(anyInt(), anyString(), anyInt(), anyString(), anyInt(), + anyString())).thenThrow(new RuntimeException("row is locked")); + + assertThrows(RuntimeException.class, () -> service.editQuestionnaire( + "{\"questionnaireDetail\":{\"questionID\":501,\"question\":\"Was it helpful?\"," + + "\"questionWeightage\":6,\"answerType\":\"Radio\",\"providerServiceMapID\":4001," + + "\"modifiedBy\":\"supervisor\",\"questionOptions\":[]}}")); + } +} diff --git a/src/test/java/com/iemr/admin/service/rolemaster/RoleMasterServiceImplTest.java b/src/test/java/com/iemr/admin/service/rolemaster/RoleMasterServiceImplTest.java new file mode 100644 index 0000000..92255fc --- /dev/null +++ b/src/test/java/com/iemr/admin/service/rolemaster/RoleMasterServiceImplTest.java @@ -0,0 +1,346 @@ +/* +* 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.admin.service.rolemaster; + +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.admin.data.rolemaster.M_Screen; +import com.iemr.admin.data.rolemaster.M_UserservicerolemappingForRoleProviderAdmin; +import com.iemr.admin.data.rolemaster.RoleMaster; +import com.iemr.admin.data.rolemaster.RoleScreenMapping; +import com.iemr.admin.data.rolemaster.StateMasterForRole; +import com.iemr.admin.data.rolemaster.StateServiceMapping; +import com.iemr.admin.repository.rolemaster.M_RoleRepo; +import com.iemr.admin.repository.rolemaster.M_ScreenRepo; +import com.iemr.admin.repository.rolemaster.M_UserservicerolemappingForRoleProviderAdminRepo; +import com.iemr.admin.repository.rolemaster.RoleMasterRepo; +import com.iemr.admin.repository.rolemaster.RoleScreenMappingRepo; +import com.iemr.admin.repository.rolemaster.StateMasterRepo; + +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The role master service reads the role catalogue and the screens each role + * unlocks, choosing a national or state-scoped query from the flag the caller + * sends. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("Role_Master_ServiceImpl Test Suite") +class RoleMasterServiceImplTest { + + private static final Integer PROVIDER_ID = 77; + private static final Integer PSM_ID = 4001; + + @Mock + private StateMasterRepo stateMasterRepo; + + @Mock + private jakarta.persistence.EntityManager entityManager; + + @Mock + private M_UserservicerolemappingForRoleProviderAdminRepo m_UserservicerolemappingForRoleProviderAdminRepo; + + @Mock + private RoleScreenMappingRepo roleScreenMappingRepo; + + @Mock + private M_ScreenRepo m_ScreenRepo; + + @Mock + private RoleMasterRepo roleMasterRepo; + + @Mock + private M_RoleRepo mRoleRepo; + + @InjectMocks + private Role_Master_ServiceImpl service; + + @Test + @DisplayName("getStateByServiceProviderId should skip a row the query could not fill") + void getStateByServiceProviderId_shouldSkipUnfillableRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { 29, "Karnataka", PSM_ID }); + when(roleMasterRepo.getStateByServiceProviderId(PROVIDER_ID)).thenReturn(rows); + + ArrayList mappings = service.getStateByServiceProviderId(PROVIDER_ID); + + assertEquals(1, mappings.size()); + assertEquals(29, mappings.get(0).getStateID()); + } + + @Test + @DisplayName("getServiceByServiceProviderIdAndStateId should rebuild one mapping per row") + void getServiceByServiceProviderIdAndStateId_shouldRebuildEachRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { PSM_ID, 3, "Tele Medicine", PROVIDER_ID }); + when(roleMasterRepo.getServiceByServiceProviderIdAndStateId(PROVIDER_ID, 29)).thenReturn(rows); + + assertEquals(1, service.getServiceByServiceProviderIdAndStateId(PROVIDER_ID, 29).size()); + } + + @Test + @DisplayName("getAllRoleByMapId should answer nothing, as the catalogue is read elsewhere") + void getAllRoleByMapId_shouldAnswerNothing() { + assertNull(service.getAllRoleByMapId()); + } + + @Test + @DisplayName("getAllByMapId should narrow by state for a state-scoped service line") + void getAllByMapId_shouldNarrowByStateForStateService() { + ArrayList stored = new ArrayList<>(); + when(roleMasterRepo.getAllByMapId(PROVIDER_ID, 29, 3)).thenReturn(stored); + + assertSame(stored, service.getAllByMapId(PROVIDER_ID, 29, 3, Boolean.FALSE)); + verify(roleMasterRepo).getAllByMapId(PROVIDER_ID, 29, 3); + } + + @Test + @DisplayName("getAllByMapId should ignore the state for a national service line") + void getAllByMapId_shouldIgnoreStateForNationalService() { + ArrayList stored = new ArrayList<>(); + when(roleMasterRepo.getAlByMapId(PROVIDER_ID, 3)).thenReturn(stored); + + assertSame(stored, service.getAllByMapId(PROVIDER_ID, 29, 3, Boolean.TRUE)); + verify(roleMasterRepo).getAlByMapId(PROVIDER_ID, 3); + } + + @Test + @DisplayName("getAllByMapId with two arguments should ignore the state altogether") + void getAllByMapId_twoArguments_shouldIgnoreState() { + ArrayList stored = new ArrayList<>(); + when(roleMasterRepo.getAlByMapId(PROVIDER_ID, 3)).thenReturn(stored); + + assertSame(stored, service.getAllByMapId(PROVIDER_ID, 3)); + } + + @Test + @DisplayName("getProStateServRoles should skip a row the query could not fill") + void getProStateServRoles_shouldSkipUnfillableRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { 11, "Counsellor", "Handles calls", Boolean.FALSE, "admin", PSM_ID }); + when(roleScreenMappingRepo.getAllRoleByMapId(PSM_ID)).thenReturn(rows); + + ArrayList roles = service.getProStateServRoles(PSM_ID); + + assertEquals(1, roles.size()); + assertEquals("Counsellor", roles.get(0).getRoleName()); + } + + @Test + @DisplayName("getRoleMasterTM should read the same catalogue as the state and service search") + void getRoleMasterTM_shouldReadTheSameCatalogue() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { 11, "TC Specialist", "Telemedicine", Boolean.FALSE, "admin", PSM_ID }); + when(roleScreenMappingRepo.getAllRoleByMapId(PSM_ID)).thenReturn(rows); + + assertEquals("TC Specialist", service.getRoleMasterTM(PSM_ID).get(0).getRoleName()); + } + + @Test + @DisplayName("getProStateServRolesV1 should convert each stored role the query answers") + void getProStateServRolesV1_shouldConvertEachRole() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { java.util.Map.of("roleID", 11, "roleName", "Counsellor") }); + when(mRoleRepo.getAllRoleByMapId1(PSM_ID)).thenReturn(rows); + + ArrayList roles = service.getProStateServRolesV1(PSM_ID); + + assertEquals(1, roles.size()); + assertEquals("Counsellor", roles.get(0).getRoleName()); + } + + @Test + @DisplayName("getProStateServRoles1 should convert each stored role the query answers") + void getProStateServRoles1_shouldConvertEachRole() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { java.util.Map.of("roleID", 11, "roleName", "Counsellor") }); + when(mRoleRepo.getAllRoleByMapId1(PSM_ID)).thenReturn(rows); + + assertEquals(1, service.getProStateServRoles1(PSM_ID).size()); + } + + @Test + @DisplayName("getProStateServRoles1 should answer nothing when the query answers nothing") + void getProStateServRoles1_shouldAnswerNothingForNullResult() { + when(mRoleRepo.getAllRoleByMapId1(PSM_ID)).thenReturn(null); + + assertTrue(service.getProStateServRoles1(PSM_ID).isEmpty()); + } + + @Test + @DisplayName("the role writes should each reach their own repository") + void roleWrites_shouldReachTheirOwnRepository() { + RoleMaster role = new RoleMaster(); + role.setRoleID(11); + ArrayList roles = new ArrayList<>(List.of(role)); + when(mRoleRepo.saveAll(anyList())).thenReturn(roles); + when(mRoleRepo.getRoleByRoleId(11)).thenReturn(role); + when(mRoleRepo.save(role)).thenReturn(role); + when(mRoleRepo.findByDeletedAndProviderServiceMapID(false, PSM_ID)).thenReturn(roles); + + assertEquals(1, service.addRole(new ArrayList<>()).size()); + assertSame(role, service.getRoleByRoleId(11)); + assertSame(role, service.modifydata(role)); + assertEquals("success", service.deletedata(role)); + assertSame(roles, service.getProStateServRolesActive(PSM_ID)); + } + + @Test + @DisplayName("getAllFeature and getAllState should hand back what the repositories hold") + void masters_shouldHandBackRepositoryContents() { + ArrayList screens = new ArrayList<>(List.of(new M_Screen())); + ArrayList states = new ArrayList<>(List.of(new StateMasterForRole())); + when(m_ScreenRepo.getAllFeature(3)).thenReturn(screens); + when(stateMasterRepo.getAllState()).thenReturn(states); + + assertSame(screens, service.getAllFeature(3)); + assertSame(states, service.getAllState()); + } + + @Test + @DisplayName("settingScreenId should report whether the screen mapping actually moved") + void settingScreenId_shouldReportWhetherMappingMoved() { + when(roleScreenMappingRepo.updatescreenId(31, 21)).thenReturn(1); + when(roleScreenMappingRepo.updatescreenId(32, 21)).thenReturn(0); + + assertEquals("success", service.settingScreenId(31, 21)); + assertEquals("fail", service.settingScreenId(32, 21)); + } + + @Test + @DisplayName("mapfeature should hand its batch to the screen mapping repository") + void mapfeature_shouldHandBatchToRepository() { + ArrayList stored = new ArrayList<>(List.of(new RoleScreenMapping())); + when(roleScreenMappingRepo.saveAll(anyList())).thenReturn(stored); + + assertEquals(1, service.mapfeature(new ArrayList<>()).size()); + } + + @Test + @DisplayName("getServiceByServiceProviderIds should skip a row the query could not fill") + void getServiceByServiceProviderIds_shouldSkipUnfillableRow() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { "Tele Medicine", 3, Boolean.FALSE, PSM_ID }); + when(m_UserservicerolemappingForRoleProviderAdminRepo.getServiceByServiceProviderIds(3117)) + .thenReturn(rows); + + assertEquals(1, service.getServiceByServiceProviderIds(3117).size()); + } + + @Test + @DisplayName("getStateByServiceProviderIdAndServiceLines should read the state mappings for a state service") + void getStateByServiceLines_shouldReadStateMappings() { + ArrayList rows = new ArrayList<>(); + rows.add(null); + rows.add(new Object[] { PSM_ID, "Karnataka", 29, 3 }); + when(m_UserservicerolemappingForRoleProviderAdminRepo + .getStateByServiceProviderIdAndServiceLines(3117, 3)).thenReturn(rows); + + ArrayList mappings = + service.getStateByServiceProviderIdAndServiceLines(3117, 3, Boolean.FALSE); + + assertEquals(1, mappings.size()); + } + + @Test + @DisplayName("getStateByServiceProviderIdAndServiceLines should list every state for a national service") + void getStateByServiceLines_shouldListEveryStateForNationalService() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { PSM_ID }); + StateMasterForRole karnataka = new StateMasterForRole(); + karnataka.setStateID(29); + karnataka.setStateName("Karnataka"); + StateMasterForRole kerala = new StateMasterForRole(); + kerala.setStateID(32); + kerala.setStateName("Kerala"); + when(m_UserservicerolemappingForRoleProviderAdminRepo + .getStateByServiceProviderIdAndServiceLines1(3117, 3)).thenReturn(rows); + when(stateMasterRepo.getAllState()).thenReturn(new ArrayList<>(List.of(karnataka, kerala))); + + ArrayList mappings = + service.getStateByServiceProviderIdAndServiceLines(3117, 3, Boolean.TRUE); + + assertEquals(2, mappings.size(), "a national service line reaches every state on record"); + } + + @Test + @DisplayName("configWrapUpTime should carry the new wrap-up settings onto the stored role") + void configWrapUpTime_shouldCarryNewSettings() throws Exception { + RoleMaster stored = new RoleMaster(); + stored.setRoleID(11); + RoleMaster request = new RoleMaster(); + request.setRoleID(11); + request.setIsWrapUpTime(Boolean.TRUE); + request.setWrapUpTime(30); + request.setModifiedBy("admin"); + when(mRoleRepo.findByRoleID(11)).thenReturn(stored); + when(mRoleRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.configWrapUpTime(request)); + assertEquals(30, stored.getWrapUpTime()); + assertTrue(stored.getIsWrapUpTime()); + assertEquals("admin", stored.getModifiedBy()); + } + + @Test + @DisplayName("configWrapUpTime should refuse a role that does not exist") + void configWrapUpTime_shouldRefuseUnknownRole() { + RoleMaster request = new RoleMaster(); + request.setRoleID(11); + when(mRoleRepo.findByRoleID(11)).thenReturn(null); + + Exception thrown = assertThrows(Exception.class, () -> service.configWrapUpTime(request)); + assertEquals("Invalid Role", thrown.getMessage()); + } + + @Test + @DisplayName("configWrapUpTime should refuse a change that names nobody as its author") + void configWrapUpTime_shouldRefuseUnattributedChange() { + RoleMaster request = new RoleMaster(); + request.setRoleID(11); + when(mRoleRepo.findByRoleID(11)).thenReturn(new RoleMaster()); + + Exception thrown = assertThrows(Exception.class, () -> service.configWrapUpTime(request)); + assertEquals("Please provide Modified by", thrown.getMessage()); + } +} diff --git a/src/test/java/com/iemr/admin/service/servicePoint/ServicePointServiceImplTest.java b/src/test/java/com/iemr/admin/service/servicePoint/ServicePointServiceImplTest.java new file mode 100644 index 0000000..bf474d2 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/servicePoint/ServicePointServiceImplTest.java @@ -0,0 +1,197 @@ +/* +* 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.admin.service.servicePoint; + +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.admin.data.locationmaster.DistrictBranchMapping; +import com.iemr.admin.data.servicePoint.M_Servicepoint; +import com.iemr.admin.data.servicePoint.M_Servicepointvillagemap; +import com.iemr.admin.repo.locationmaster.DistrictBranchMappingRepo; +import com.iemr.admin.repository.servicePoint.ServicePointRepository; +import com.iemr.admin.repository.servicePoint.ServicePointVillageMapRepository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The service point service turns partly-filled location filters into wildcard + * queries, so an operator who leaves a filter blank still sees every match. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ServicePointServiceImpl Test Suite") +class ServicePointServiceImplTest { + + private static final Integer PROVIDER_ID = 77; + private static final Integer PSM_ID = 4001; + private static final Integer POINT_ID = 71; + + @Mock + private ServicePointRepository servicePointRepository; + + @Mock + private DistrictBranchMappingRepo districtBranchMappingRepo; + + @Mock + private ServicePointVillageMapRepository servicePointVillageMapRepository; + + @InjectMocks + private ServicePointServiceImpl service; + + private static Object[] pointRow() { + return new Object[] { POINT_ID, "Hosur halt", "Weekly halt", "Main Road", PSM_ID, Boolean.FALSE, + 29, "Karnataka", 301, "Bengaluru Urban", 401, "North block", 501, "Hosur", 1, "India", null, + 3, "Mobile Medical Unit", 31, "Hosur parking" }; + } + + private static Object[] villageRow() { + return new Object[] { 9001, 29, "Karnataka", 301, "Bengaluru Urban", 401, "North block", 501, "Hosur", + POINT_ID, "Hosur halt", PSM_ID, Boolean.FALSE, null, null, 31, "Hosur parking" }; + } + + @Test + @DisplayName("saveServicePoint should hand its batch to the repository") + void saveServicePoint_shouldHandBatchToRepository() { + ArrayList stored = new ArrayList<>(List.of(new M_Servicepoint())); + when(servicePointRepository.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.saveServicePoint(new ArrayList<>())); + } + + @Test + @DisplayName("getAvailableServicePoints should pass every filter the caller supplies through") + void getAvailableServicePoints_shouldPassFiltersThrough() { + when(servicePointRepository.getAvailableServicePoints("29", "301", "31", PROVIDER_ID)) + .thenReturn(List.of(pointRow())); + + assertEquals(1, service.getAvailableServicePoints(29, 301, 31, PROVIDER_ID).size()); + } + + @Test + @DisplayName("getAvailableServicePoints should match everything for a filter the caller leaves blank") + void getAvailableServicePoints_shouldWildcardBlankFilters() { + when(servicePointRepository.getAvailableServicePoints("%%", "%%", "%%", PROVIDER_ID)) + .thenReturn(new ArrayList<>()); + + service.getAvailableServicePoints(null, null, null, PROVIDER_ID); + + verify(servicePointRepository).getAvailableServicePoints("%%", "%%", "%%", PROVIDER_ID); + } + + @Test + @DisplayName("getAvailableServicePointVillageMaps should pass every filter the caller supplies through") + void getVillageMaps_shouldPassFiltersThrough() { + when(servicePointVillageMapRepository + .getAvailableServicePointVillageMaps("29", "301", "31", "71", PROVIDER_ID)) + .thenReturn(List.of(villageRow())); + + assertEquals(1, service.getAvailableServicePointVillageMaps(29, 301, 31, POINT_ID, PROVIDER_ID).size()); + } + + @Test + @DisplayName("getAvailableServicePointVillageMaps should match everything for filters left blank") + void getVillageMaps_shouldWildcardBlankFilters() { + when(servicePointVillageMapRepository + .getAvailableServicePointVillageMaps("%%", "%%", "%%", "%%", PROVIDER_ID)) + .thenReturn(new ArrayList<>()); + + service.getAvailableServicePointVillageMaps(null, null, null, null, PROVIDER_ID); + + verify(servicePointVillageMapRepository) + .getAvailableServicePointVillageMaps("%%", "%%", "%%", "%%", PROVIDER_ID); + } + + @Test + @DisplayName("the status updates should each reach their own repository query") + void statusUpdates_shouldReachTheirOwnQuery() { + M_Servicepoint point = new M_Servicepoint(POINT_ID, "Hosur halt", "Weekly halt", "Main Road", PSM_ID, + Boolean.TRUE, 1, "India", 29, "Karnataka", 301, "Bengaluru Urban", 401, "North block", 501, + "Hosur", null, 3, "Mobile Medical Unit", 31, "Hosur parking"); + point.setModifiedBy("admin"); + M_Servicepointvillagemap map = new M_Servicepointvillagemap(9001, 29, "Karnataka", 301, + "Bengaluru Urban", 31, "Hosur parking", POINT_ID, "Hosur halt", 501, "Hosur", PSM_ID, + Boolean.TRUE); + map.setModifiedBy("admin"); + when(servicePointRepository.updateServicePointStatus(POINT_ID, Boolean.TRUE, "admin")).thenReturn(1); + when(servicePointVillageMapRepository.updateServicePointStatus(9001, Boolean.TRUE, "admin")).thenReturn(1); + + assertEquals(1, service.updateServicePointStatus(point)); + assertEquals(1, service.updateServicePointVillageMapStatus(map)); + } + + @Test + @DisplayName("the record lookups should each reach their own repository query") + void recordLookups_shouldReachTheirOwnQuery() { + M_Servicepoint point = new M_Servicepoint(); + M_Servicepointvillagemap map = new M_Servicepointvillagemap(); + ArrayList maps = new ArrayList<>(List.of(map)); + when(servicePointRepository.findByServicePointID(POINT_ID)).thenReturn(point); + when(servicePointRepository.save(point)).thenReturn(point); + when(servicePointVillageMapRepository.findByServicePointVillageMapID(9001)).thenReturn(map); + when(servicePointVillageMapRepository.save(map)).thenReturn(map); + when(servicePointVillageMapRepository.saveAll(anyList())).thenReturn(maps); + + assertSame(point, service.getdataForEditServicePointStatus(POINT_ID)); + assertSame(point, service.saveeditedData(point)); + assertSame(map, service.updateServicePointVillageMapStatus(9001)); + assertSame(map, service.saveEditedData(map)); + assertSame(maps, service.saveServicePointVillageMap(new ArrayList<>())); + } + + @Test + @DisplayName("getunmappedvillages should exclude the villages already covered when there are any") + void getunmappedvillages_shouldExcludeCoveredVillages() { + List expected = List.of(new DistrictBranchMapping()); + when(servicePointVillageMapRepository.finbyTalukID(PSM_ID)).thenReturn(List.of(501)); + when(districtBranchMappingRepo.getunmappedvillage(List.of(501), 401)).thenReturn(expected); + + assertSame(expected, service.getunmappedvillages(PSM_ID, 401)); + verify(districtBranchMappingRepo, never()).getallvillage(anyInt()); + } + + @Test + @DisplayName("getunmappedvillages should answer every village when none is covered yet") + void getunmappedvillages_shouldAnswerEveryVillageWhenNoneCovered() { + List expected = List.of(new DistrictBranchMapping()); + when(servicePointVillageMapRepository.finbyTalukID(PSM_ID)).thenReturn(new ArrayList<>()); + when(districtBranchMappingRepo.getallvillage(401)).thenReturn(expected); + + assertSame(expected, service.getunmappedvillages(PSM_ID, 401)); + } +} diff --git a/src/test/java/com/iemr/admin/service/snomedMapping/SnomedServiceImplTest.java b/src/test/java/com/iemr/admin/service/snomedMapping/SnomedServiceImplTest.java new file mode 100644 index 0000000..d11bc17 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/snomedMapping/SnomedServiceImplTest.java @@ -0,0 +1,273 @@ +/* +* 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.admin.service.snomedMapping; + +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.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.admin.data.snomedMapping.ChildVaccinations; +import com.iemr.admin.data.snomedMapping.DiseaseType; +import com.iemr.admin.data.snomedMapping.OptionalVaccinations; +import com.iemr.admin.repository.snomedRepo.SnomedImmunizationRepo; +import com.iemr.admin.repository.snomedRepo.SnomedMappingRepo; +import com.iemr.admin.repository.snomedRepo.SnomedVaccinationRepo; + +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.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The snomed service maps three separate clinical masters onto SNOMED codes, + * choosing the master to work on from the request's own master type. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("SnomedServiceImpl Test Suite") +class SnomedServiceImplTest { + + private static final Short MASTER_ID = 12; + + @Mock + private SnomedMappingRepo snomedFamilyHistoryRepo; + + @Mock + private SnomedVaccinationRepo snomedVaccinationRepo; + + @Mock + private SnomedImmunizationRepo snomedImmunizationRepo; + + @InjectMocks + private SnomedServiceImpl service; + + private static String requestFor(String masterType) { + return "{\"masterType\":\"" + masterType + "\",\"masterID\":12,\"sctCode\":\"73211009\"," + + "\"sctTerm\":\"Diabetes mellitus\",\"modifiedBy\":\"admin\",\"deleted\":false," + + "\"mappingDetails\":[{\"masterID\":12,\"sctCode\":\"73211009\"}]}"; + } + + private static JsonObject asJson(String request) { + return JsonParser.parseString(request).getAsJsonObject(); + } + + @Test + @DisplayName("editSnomedMappingData should record the code against the family history master") + void edit_shouldRecordCodeAgainstFamilyHistory() { + String request = requestFor("Family History"); + when(snomedFamilyHistoryRepo.updateFamilyHistoryDetails(MASTER_ID, "73211009", "Diabetes mellitus", "admin")) + .thenReturn(1); + + assertEquals("Data Updated", service.editSnomedMappingData(asJson(request), request)); + } + + @Test + @DisplayName("editSnomedMappingData should record the code against the optional vaccination master") + void edit_shouldRecordCodeAgainstOptionalVaccination() { + String request = requestFor("Optional Vaccination"); + when(snomedVaccinationRepo.updateVaccinationDetails(any(), anyString(), anyString(), anyString())) + .thenReturn(1); + + assertEquals("Data Updated", service.editSnomedMappingData(asJson(request), request)); + } + + @Test + @DisplayName("editSnomedMappingData should record the code against the immunization master") + void edit_shouldRecordCodeAgainstImmunization() { + String request = requestFor("Immunization"); + when(snomedImmunizationRepo.updateImmunizationDetails(any(), anyString(), anyString(), anyString())) + .thenReturn(1); + + assertEquals("Data Updated", service.editSnomedMappingData(asJson(request), request)); + } + + @Test + @DisplayName("editSnomedMappingData should give up on a master type it does not recognise") + void edit_shouldGiveUpOnUnknownMasterType() { + String request = requestFor("Astrology"); + + assertThrows(NullPointerException.class, () -> service.editSnomedMappingData(asJson(request), request), + "no master was touched, so there is no row count to judge the edit by"); + } + + @Test + @DisplayName("editSnomedMappingData should refuse a request that is not there at all") + void edit_shouldRefuseAbsentRequest() { + assertEquals("Invalid Master Type", service.editSnomedMappingData(null, null)); + } + + @Test + @DisplayName("editSnomedMappingData should give up when the master row could not be touched") + void edit_shouldGiveUpWhenNothingTouched() { + String request = requestFor("Family History"); + when(snomedFamilyHistoryRepo.updateFamilyHistoryDetails(any(), anyString(), anyString(), anyString())) + .thenReturn(null); + + assertThrows(NullPointerException.class, () -> service.editSnomedMappingData(asJson(request), request)); + } + + @Test + @DisplayName("saveSnomedMappingData should store the family history mappings the request carried") + void save_shouldStoreFamilyHistoryMappings() { + String request = requestFor("Family History"); + when(snomedFamilyHistoryRepo.saveAll(anyList())).thenReturn(List.of(new DiseaseType())); + + assertEquals("Data Saved", service.saveSnomedMappingData(asJson(request), request)); + } + + @Test + @DisplayName("saveSnomedMappingData should store the optional vaccination mappings the request carried") + void save_shouldStoreOptionalVaccinationMappings() { + String request = requestFor("Optional Vaccination"); + when(snomedVaccinationRepo.saveAll(anyList())).thenReturn(List.of(new OptionalVaccinations())); + + assertEquals("Data Saved", service.saveSnomedMappingData(asJson(request), request)); + } + + @Test + @DisplayName("saveSnomedMappingData should store the immunization mappings the request carried") + void save_shouldStoreImmunizationMappings() { + String request = requestFor("Immunization"); + when(snomedImmunizationRepo.saveAll(anyList())).thenReturn(List.of(new ChildVaccinations())); + + assertEquals("Data Saved", service.saveSnomedMappingData(asJson(request), request)); + } + + @Test + @DisplayName("saveSnomedMappingData should say nothing was saved when the repository stored nothing") + void save_shouldSayNothingSavedWhenRepositoryStoredNothing() { + String request = requestFor("Family History"); + when(snomedFamilyHistoryRepo.saveAll(anyList())).thenReturn(new ArrayList()); + + assertEquals(null, service.saveSnomedMappingData(asJson(request), request)); + } + + @Test + @DisplayName("saveSnomedMappingData should refuse a master type it does not recognise") + void save_shouldRefuseUnknownMasterType() { + String request = requestFor("Astrology"); + + assertEquals("Invalid Master Type", service.saveSnomedMappingData(asJson(request), request)); + } + + @Test + @DisplayName("saveSnomedMappingData should refuse a request that is not there at all") + void save_shouldRefuseAbsentRequest() { + assertEquals("Invalid Master Type", service.saveSnomedMappingData(null, null)); + } + + @Test + @DisplayName("fetchSnomedMaster should answer the family history master as it stands") + void fetch_shouldAnswerFamilyHistoryMaster() { + DiseaseType entry = new DiseaseType(); + entry.setMasterName("Diabetes"); + when(snomedFamilyHistoryRepo.fetchDiseaseType()).thenReturn(List.of(entry)); + + assertTrue(service.fetchSnomedMaster(asJson(requestFor("Family History"))).contains("Diabetes")); + } + + @Test + @DisplayName("fetchSnomedMaster should answer the optional vaccination master as it stands") + void fetch_shouldAnswerOptionalVaccinationMaster() { + OptionalVaccinations entry = new OptionalVaccinations(); + entry.setMasterName("Typhoid"); + when(snomedVaccinationRepo.fetchOptionalVaccinations()).thenReturn(List.of(entry)); + + assertTrue(service.fetchSnomedMaster(asJson(requestFor("Optional Vaccination"))).contains("Typhoid")); + } + + @Test + @DisplayName("fetchSnomedMaster should answer the immunization master as it stands") + void fetch_shouldAnswerImmunizationMaster() { + ChildVaccinations entry = new ChildVaccinations(); + entry.setMasterName("BCG"); + when(snomedImmunizationRepo.fetchChildVaccinations()).thenReturn(List.of(entry)); + + assertTrue(service.fetchSnomedMaster(asJson(requestFor("Immunization"))).contains("BCG")); + } + + @Test + @DisplayName("fetchSnomedMaster should refuse a master type it does not recognise") + void fetch_shouldRefuseUnknownMasterType() { + assertEquals("Invalid Master Type", service.fetchSnomedMaster(asJson(requestFor("Astrology")))); + } + + @Test + @DisplayName("fetchSnomedMaster should refuse a request that is not there at all") + void fetch_shouldRefuseAbsentRequest() { + assertEquals("Invalid request", service.fetchSnomedMaster(null)); + } + + @Test + @DisplayName("updateStatus should retire the entry in whichever master the request names") + void updateStatus_shouldRetireEntryInNamedMaster() { + when(snomedFamilyHistoryRepo.updateStatus(any(), anyBoolean(), anyString())).thenReturn(1); + + assertEquals("Data updated successfully", service.updateStatus(requestFor("Family History"))); + verify(snomedFamilyHistoryRepo).updateStatus(MASTER_ID, Boolean.FALSE, "admin"); + } + + @Test + @DisplayName("updateStatus should retire the entry in the optional vaccination master") + void updateStatus_shouldRetireOptionalVaccinationEntry() { + when(snomedVaccinationRepo.updateStatus(any(), anyBoolean(), anyString())).thenReturn(1); + + assertEquals("Data updated successfully", service.updateStatus(requestFor("Optional Vaccination"))); + } + + @Test + @DisplayName("updateStatus should retire the entry in the immunization master") + void updateStatus_shouldRetireImmunizationEntry() { + when(snomedImmunizationRepo.updateStatus(any(), anyBoolean(), anyString())).thenReturn(1); + + assertEquals("Data updated successfully", service.updateStatus(requestFor("Immunization"))); + } + + @Test + @DisplayName("updateStatus should say so when no entry was touched") + void updateStatus_shouldSaySoWhenNothingTouched() { + when(snomedFamilyHistoryRepo.updateStatus(any(), anyBoolean(), anyString())).thenReturn(0); + + assertEquals("Data not updated", service.updateStatus(requestFor("Family History"))); + } + + @Test + @DisplayName("updateStatus should touch no master when the master type is not recognised") + void updateStatus_shouldTouchNoMasterForUnknownType() { + assertEquals("Data not updated", service.updateStatus(requestFor("Astrology"))); + } +} diff --git a/src/test/java/com/iemr/admin/service/stockEntry/StockEntryServiceImplTest.java b/src/test/java/com/iemr/admin/service/stockEntry/StockEntryServiceImplTest.java new file mode 100644 index 0000000..acde778 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/stockEntry/StockEntryServiceImplTest.java @@ -0,0 +1,318 @@ +/* +* 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.admin.service.stockEntry; + +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 org.springframework.dao.DataIntegrityViolationException; + +import com.iemr.admin.data.items.ItemMaster; +import com.iemr.admin.data.items.M_ItemCategory; +import com.iemr.admin.data.stockentry.ItemStockEntry; +import com.iemr.admin.data.stockentry.PhysicalStockEntry; +import com.iemr.admin.data.stockExit.ItemStockExit; +import com.iemr.admin.repo.stockEntry.ItemStockEntryRepo; +import com.iemr.admin.repo.stockEntry.PhysicalStockEntryRepo; +import com.iemr.admin.service.item.ItemService; + +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.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; + +/** + * The stock entry service records what arrives at a store, and works out which + * batches an issue should draw on given the item category's issue rule. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("StockEntryServiceImpl Test Suite") +class StockEntryServiceImplTest { + + private static final Integer FACILITY_ID = 9001; + private static final Integer ITEM_ID = 501; + private static final Integer ENTRY_ID = 7001; + + @Mock + private PhysicalStockEntryRepo physicalStockEntryRepo; + + @Mock + private ItemStockEntryRepo itemStockEntryRepo; + + @Mock + private ItemService itemService; + + @InjectMocks + private StockEntryServiceImpl service; + + private static ItemStockEntry batch(Integer stockEntryID, Integer quantityInHand) { + ItemStockEntry batch = new ItemStockEntry(); + batch.setItemStockEntryID(stockEntryID); + batch.setItemID(ITEM_ID); + batch.setFacilityID(FACILITY_ID); + batch.setQuantity(quantityInHand); + batch.setQuantityInHand(quantityInHand); + return batch; + } + + private static PhysicalStockEntry arrival() { + PhysicalStockEntry arrival = new PhysicalStockEntry(); + arrival.setPhyEntryID(ENTRY_ID); + arrival.setRefNo("GRN-101"); + arrival.setItemStockEntry(new ArrayList<>(List.of(batch(null, 100)))); + return arrival; + } + + private static ItemMaster itemIssuedBy(String issueType) { + M_ItemCategory category = new M_ItemCategory(); + category.setIssueType(issueType); + ItemMaster item = new ItemMaster(); + item.setItemCategory(category); + return item; + } + + private static ItemStockExit demandFor(int quantity) { + ItemStockExit demand = new ItemStockExit(); + demand.setItemID(ITEM_ID); + demand.setQuantity(quantity); + return demand; + } + + @Test + @DisplayName("savePhysicalStockEntry should tie each batch to the arrival and put its quantity in hand") + void save_shouldTieBatchesToArrival() { + PhysicalStockEntry arrival = arrival(); + when(physicalStockEntryRepo.save(arrival)).thenReturn(arrival); + when(itemStockEntryRepo.saveAll(anyList())).thenAnswer(call -> new ArrayList<>(call.getArgument(0))); + + PhysicalStockEntry stored = service.savePhysicalStockEntry(arrival); + + assertEquals(1, stored.getItemStockEntry().size()); + ItemStockEntry batch = stored.getItemStockEntry().get(0); + assertEquals(ENTRY_ID, batch.getEntryTypeID()); + assertEquals("physicalStockEntry", batch.getEntryType()); + assertEquals(100, batch.getQuantityInHand()); + } + + @Test + @DisplayName("savePhysicalStockEntry should abandon the whole arrival when a batch clashes with one on file") + void save_shouldAbandonArrivalOnClashingBatch() { + PhysicalStockEntry arrival = arrival(); + when(physicalStockEntryRepo.save(arrival)).thenReturn(arrival); + when(itemStockEntryRepo.saveAll(anyList())) + .thenThrow(new DataIntegrityViolationException("duplicate batch number")); + + assertThrows(DataIntegrityViolationException.class, () -> service.savePhysicalStockEntry(arrival)); + verify(physicalStockEntryRepo).updateDelete(ENTRY_ID, true); + assertTrue(arrival.getDeleted()); + } + + @Test + @DisplayName("savePhysicalStockEntry should keep the arrival when the batches fail for some other reason") + void save_shouldKeepArrivalOnOtherFailure() { + PhysicalStockEntry arrival = arrival(); + when(physicalStockEntryRepo.save(arrival)).thenReturn(arrival); + when(itemStockEntryRepo.saveAll(anyList())).thenThrow(new RuntimeException("connection reset")); + + assertSame(arrival, service.savePhysicalStockEntry(arrival)); + verify(physicalStockEntryRepo, org.mockito.Mockito.never()).updateDelete(anyInt(), anyBoolean()); + } + + @Test + @DisplayName("getItemBatchForStoreID should answer only the batches the store still holds") + void getItemBatch_shouldAnswerBatchesStillHeld() { + List held = List.of(batch(1, 10)); + when(itemStockEntryRepo.findByFacilityIDAndItemIDAndQuantityInHandGreaterThanAndDeleted(FACILITY_ID, ITEM_ID, + 0, false)).thenReturn(held); + + ItemStockEntry request = new ItemStockEntry(); + request.setFacilityID(FACILITY_ID); + request.setItemID(ITEM_ID); + + assertSame(held, service.getItemBatchForStoreID(request)); + } + + @Test + @DisplayName("getAllItemBatchForStoreID should answer the running totals the query worked out") + void getAllItemBatch_shouldAnswerRunningTotals() { + ArrayList totals = new ArrayList<>(List.of(new Object[] { ITEM_ID, 100 })); + when(itemStockEntryRepo.getQuantityOfStock(new Integer[] { 1 }, FACILITY_ID)).thenReturn(totals); + + assertEquals(1, service.getAllItemBatchForStoreID(FACILITY_ID, new Integer[] { 1 }).size()); + } + + @Test + @DisplayName("updateStocks should draw each issued quantity off the batch it came from") + void updateStocks_shouldDrawIssuedQuantityOffBatch() { + ItemStockExit issued = demandFor(30); + issued.setItemStockEntryID(1); + issued.setQuantityInHand(100); + when(itemStockEntryRepo.updateStock(1, 70)).thenReturn(1); + + assertEquals(1, service.updateStocks(List.of(issued))); + verify(itemStockEntryRepo).updateStock(1, 70); + } + + @Test + @DisplayName("updateStocks should count nothing when there is nothing to draw off") + void updateStocks_shouldCountNothingForEmptyIssue() { + assertEquals(0, service.updateStocks(new ArrayList<>())); + } + + @Test + @DisplayName("the ordered batch lookups should each reach their own repository query") + void orderedLookups_shouldReachTheirOwnQuery() { + List byEntryAsc = List.of(batch(1, 10)); + List byEntryDesc = List.of(batch(2, 10)); + List byExpiry = List.of(batch(3, 10)); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByCreatedByAsc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(byEntryAsc); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByCreatedByDesc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(byEntryDesc); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByExpiryDateDesc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(byExpiry); + + assertSame(byEntryAsc, service.getItemStockForStoreIDOrderByEntryDateAsc(FACILITY_ID, ITEM_ID)); + assertSame(byEntryDesc, service.getItemStockForStoreIDOrderByEntryDateDesc(FACILITY_ID, ITEM_ID)); + assertSame(byExpiry, service.getItemStockForStoreIDOrderByExpiryDate(FACILITY_ID, ITEM_ID)); + } + + @Test + @DisplayName("getItemStockFromItemID should draw on the batch expiring first when the category says so") + void allocate_shouldDrawOnFirstExpiringBatch() { + when(itemService.getItemMasterCatByID(ITEM_ID)).thenReturn(itemIssuedBy("First Expiry First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByExpiryDateDesc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(List.of(batch(1, 100))); + + List allocated = service.getItemStockFromItemID(FACILITY_ID, List.of(demandFor(30))); + + assertEquals(1, allocated.size()); + assertEquals(30, allocated.get(0).getQuantity(), "only what the issue asks for is drawn from the batch"); + } + + @Test + @DisplayName("getItemStockFromItemID should draw on the newest batch when the category issues last in first out") + void allocate_shouldDrawOnNewestBatch() { + when(itemService.getItemMasterCatByID(ITEM_ID)).thenReturn(itemIssuedBy("Last in First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByCreatedByAsc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(List.of(batch(1, 100))); + + assertEquals(1, service.getItemStockFromItemID(FACILITY_ID, List.of(demandFor(30))).size()); + } + + @Test + @DisplayName("getItemStockFromItemID should draw on the oldest batch when the category issues first in first out") + void allocate_shouldDrawOnOldestBatch() { + when(itemService.getItemMasterCatByID(ITEM_ID)).thenReturn(itemIssuedBy("First in First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByCreatedByDesc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(List.of(batch(1, 100))); + + assertEquals(1, service.getItemStockFromItemID(FACILITY_ID, List.of(demandFor(30))).size()); + } + + @Test + @DisplayName("getItemStockFromItemID should fall back to entry order for a category with no issue rule of its own") + void allocate_shouldFallBackToEntryOrder() { + when(itemService.getItemMasterCatByID(ITEM_ID)).thenReturn(itemIssuedBy("Anything else")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByCreatedByAsc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(List.of(batch(1, 100))); + + assertEquals(1, service.getItemStockFromItemID(FACILITY_ID, List.of(demandFor(30))).size()); + } + + @Test + @DisplayName("getItemStockFromItemID should spread a large issue across as many batches as it needs") + void allocate_shouldSpreadIssueAcrossBatches() { + when(itemService.getItemMasterCatByID(ITEM_ID)).thenReturn(itemIssuedBy("First in First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByCreatedByDesc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(List.of(batch(1, 20), batch(2, 50), batch(3, 40))); + + List allocated = service.getItemStockFromItemID(FACILITY_ID, List.of(demandFor(60))); + + assertEquals(2, allocated.size(), "the third batch is left alone once the issue is covered"); + assertEquals(20, allocated.get(0).getQuantity()); + assertEquals(40, allocated.get(1).getQuantity()); + } + + @Test + @DisplayName("getItemStockFromItemID should record a shortage when the store cannot cover the issue") + void allocate_shouldRecordShortage() { + when(itemService.getItemMasterCatByID(ITEM_ID)).thenReturn(itemIssuedBy("First in First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByCreatedByDesc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(List.of(batch(1, 20))); + + List allocated = service.getItemStockFromItemID(FACILITY_ID, List.of(demandFor(50))); + + assertEquals(2, allocated.size()); + ItemStockEntry shortage = allocated.get(1); + assertEquals(30, shortage.getQuantity(), "the shortfall is carried as a batch of its own"); + assertEquals(ITEM_ID, shortage.getItemID()); + assertEquals(FACILITY_ID, shortage.getFacilityID()); + } + + @Test + @DisplayName("getItemStockFromItemID should record the whole issue as a shortage when the store holds none") + void allocate_shouldRecordWholeIssueAsShortage() { + when(itemService.getItemMasterCatByID(ITEM_ID)).thenReturn(itemIssuedBy("First in First Out")); + when(itemStockEntryRepo + .findByFacilityIDAndItemIDAndDeletedAndQuantityInHandGreaterThanOrderByCreatedByDesc(FACILITY_ID, + ITEM_ID, false, 0)).thenReturn(new ArrayList<>()); + + List allocated = service.getItemStockFromItemID(FACILITY_ID, List.of(demandFor(50))); + + assertEquals(1, allocated.size()); + assertEquals(50, allocated.get(0).getQuantity()); + } + + @Test + @DisplayName("getItemStockFromItemID should give up when the item is not on the item master") + void allocate_shouldGiveUpForUnknownItem() { + when(itemService.getItemMasterCatByID(ITEM_ID)).thenReturn(null); + + assertThrows(NullPointerException.class, + () -> service.getItemStockFromItemID(FACILITY_ID, List.of(demandFor(50)))); + } +} diff --git a/src/test/java/com/iemr/admin/service/stockExit/StockExitServiceImplTest.java b/src/test/java/com/iemr/admin/service/stockExit/StockExitServiceImplTest.java new file mode 100644 index 0000000..e1ad35c --- /dev/null +++ b/src/test/java/com/iemr/admin/service/stockExit/StockExitServiceImplTest.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.admin.service.stockExit; + +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.admin.data.stockExit.ItemStockExit; +import com.iemr.admin.data.stockExit.T_PatientIssue; +import com.iemr.admin.repo.stockExit.ItemStockExitRepo; +import com.iemr.admin.repo.stockExit.PatientIssueRepo; +import com.iemr.admin.service.item.ItemService; +import com.iemr.admin.service.stockEntry.StockEntryService; + +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The stock exit service issues drugs to a patient, but only once it has + * checked every line against what the store actually holds. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("StockExitServiceImpl Test Suite") +class StockExitServiceImplTest { + + private static final Integer FACILITY_ID = 9001; + private static final Integer ISSUE_ID = 5501; + private static final Integer STOCK_ENTRY_ID = 1; + private static final Integer ITEM_ID = 501; + + @Mock + private StockEntryService stockEntryService; + + @Mock + private ItemStockExitRepo itemStockExitRepo; + + @Mock + private PatientIssueRepo patientIssueRepo; + + @Mock + private ItemService itemService; + + @InjectMocks + private StockExitServiceImpl service; + + private static ItemStockExit issueLine(int quantity) { + ItemStockExit line = new ItemStockExit(); + line.setItemStockEntryID(STOCK_ENTRY_ID); + line.setItemID(ITEM_ID); + line.setQuantity(quantity); + return line; + } + + private static T_PatientIssue patientIssue(String issueType, ItemStockExit... lines) { + T_PatientIssue issue = new T_PatientIssue(); + issue.setPatientIssueID(ISSUE_ID); + issue.setFacilityID(FACILITY_ID); + issue.setIssueType(issueType); + issue.setCreatedBy("pharmacist"); + issue.setItemStockExit(new ArrayList<>(List.of(lines))); + return issue; + } + + /** The stock query answers the store, item, name and quantity in hand. */ + private static Object[] stockInHandRow(int quantityInHand) { + return new Object[] { FACILITY_ID, STOCK_ENTRY_ID, "Paracetamol 500", quantityInHand }; + } + + @Test + @DisplayName("issuePatientDrugs should record the issue when the store can cover every line") + void issue_shouldRecordIssueWhenStoreCovers() { + T_PatientIssue issue = patientIssue("Manual", issueLine(10)); + when(stockEntryService.getAllItemBatchForStoreID(any(), any())) + .thenReturn(List.of(stockInHandRow(100))); + when(patientIssueRepo.save(issue)).thenReturn(issue); + + assertEquals(1, service.issuePatientDrugs(issue)); + verify(itemStockExitRepo).saveAll(anyList()); + verify(stockEntryService).updateStocks(anyList()); + } + + @Test + @DisplayName("issuePatientDrugs should refuse the whole issue when the store cannot cover a line") + void issue_shouldRefuseWholeIssueOnShortage() { + T_PatientIssue issue = patientIssue("Manual", issueLine(500)); + when(stockEntryService.getAllItemBatchForStoreID(any(), any())) + .thenReturn(List.of(stockInHandRow(100))); + + assertEquals(0, service.issuePatientDrugs(issue)); + verify(patientIssueRepo, never()).save(any(T_PatientIssue.class)); + verify(itemStockExitRepo, never()).saveAll(anyList()); + } + + @Test + @DisplayName("issuePatientDrugs should leave an issue it does not raise itself alone") + void issue_shouldLeaveOtherIssueTypesAlone() { + assertEquals(0, service.issuePatientDrugs(patientIssue("Prescription", issueLine(10)))); + verify(stockEntryService, never()).getAllItemBatchForStoreID(anyInt(), any()); + } + + @Test + @DisplayName("saveItemExit should stamp each line with the issue it was raised against") + void saveItemExit_shouldStampLinesWithIssue() { + List lines = new ArrayList<>(List.of(issueLine(10))); + + assertEquals(1, service.saveItemExit(lines, ISSUE_ID, "PatientIssue")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(itemStockExitRepo).saveAll(captor.capture()); + assertEquals("PatientIssue", captor.getValue().get(0).getExitType()); + assertEquals(ISSUE_ID, captor.getValue().get(0).getExitTypeID()); + verify(stockEntryService).updateStocks(lines); + } + + @Test + @DisplayName("getItemStockAndValidate should keep a line the store can cover and record who raised it") + void validate_shouldKeepCoveredLine() { + when(stockEntryService.getAllItemBatchForStoreID(any(), any())) + .thenReturn(List.of(stockInHandRow(100))); + + List kept = service.getItemStockAndValidate(List.of(issueLine(10)), FACILITY_ID, + "pharmacist"); + + assertEquals(1, kept.size()); + assertEquals(100, kept.get(0).getQuantityInHand()); + assertEquals("pharmacist", kept.get(0).getCreatedBy()); + } + + @Test + @DisplayName("getItemStockAndValidate should drop a line the store cannot cover") + void validate_shouldDropUncoveredLine() { + when(stockEntryService.getAllItemBatchForStoreID(any(), any())) + .thenReturn(List.of(stockInHandRow(5))); + + assertTrue(service.getItemStockAndValidate(List.of(issueLine(10)), FACILITY_ID, "pharmacist").isEmpty()); + } + + @Test + @DisplayName("getItemStockAndValidate should keep a line the store covers exactly") + void validate_shouldKeepExactlyCoveredLine() { + when(stockEntryService.getAllItemBatchForStoreID(any(), any())) + .thenReturn(List.of(stockInHandRow(10))); + + assertEquals(1, service.getItemStockAndValidate(List.of(issueLine(10)), FACILITY_ID, "pharmacist").size()); + } + + @Test + @DisplayName("getItemStockAndValidate should keep nothing when the store holds none of the batches asked for") + void validate_shouldKeepNothingWhenStoreHoldsNoBatch() { + when(stockEntryService.getAllItemBatchForStoreID(any(), any())).thenReturn(new ArrayList<>()); + + assertTrue(service.getItemStockAndValidate(List.of(issueLine(10)), FACILITY_ID, "pharmacist").isEmpty()); + } +} diff --git a/src/test/java/com/iemr/admin/service/store/StoreServiceImplTest.java b/src/test/java/com/iemr/admin/service/store/StoreServiceImplTest.java new file mode 100644 index 0000000..2e9f4c4 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/store/StoreServiceImplTest.java @@ -0,0 +1,664 @@ +/* +* 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.admin.service.store; + +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.admin.data.facilitytype.M_facilitytype; +import com.iemr.admin.data.parkingPlace.M_Parkingplace; +import com.iemr.admin.data.store.FacilityVillageMapping; +import com.iemr.admin.data.store.M_Facility; +import com.iemr.admin.data.store.M_facilityMap; +import com.iemr.admin.data.store.V_FetchFacility; +import com.iemr.admin.data.vanMaster.M_Van; +import com.iemr.admin.repository.facilitytype.M_facilitytypeRepo; +import com.iemr.admin.repository.parkingPlace.ParkingPlaceRepository; +import com.iemr.admin.repository.store.FacilityVillageMappingRepo; +import com.iemr.admin.repository.store.MainStoreRepo; +import com.iemr.admin.repository.store.V_FetchFacilityRepo; +import com.iemr.admin.repository.vanMaster.VanMasterRepository; +import com.iemr.admin.service.employeemaster.AshaSupervisorMappingService; + +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; +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.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +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; + +/** + * The store service maintains the facility hierarchy - which health facility + * sits under which, and which villages each one serves - so its rules decide + * whether a facility can be retired without orphaning what hangs off it. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("StoreServiceImpl Test Suite") +class StoreServiceImplTest { + + private static final Integer FACILITY_ID = 501; + private static final Integer PSM_ID = 4001; + private static final Integer BLOCK_ID = 401; + + @Mock + private MainStoreRepo mainStoreRepo; + + @Mock + private ParkingPlaceRepository parkingPlaceRepository; + + @Mock + private VanMasterRepository vanMasterRepository; + + @Mock + private V_FetchFacilityRepo fetchFacilityRepo; + + @Mock + private FacilityVillageMappingRepo facilityVillageMappingRepo; + + @Mock + private AshaSupervisorMappingService ashaSupervisorMappingService; + + @Mock + private M_facilitytypeRepo facilityTypeRepo; + + @InjectMocks + private StoreServiceImpl service; + + private static M_Facility facility(Integer id, String name) { + M_Facility facility = new M_Facility(); + facility.setFacilityID(id); + facility.setFacilityName(name); + facility.setBlockID(BLOCK_ID); + facility.setProviderServiceMapID(PSM_ID); + return facility; + } + + private static M_facilitytype facilityType(Integer id, Integer levelValue) { + M_facilitytype type = new M_facilitytype(); + type.setFacilityTypeID(id); + type.setLevelValue(levelValue); + return type; + } + + @Nested + @DisplayName("Reads") + class ReadTests { + + @Test + @DisplayName("the facility lookups should each reach their own repository query") + void facilityLookups_shouldReachTheirOwnQuery() { + M_Facility stored = facility(FACILITY_ID, "PHC North"); + ArrayList facilities = new ArrayList<>(List.of(stored)); + List asList = List.of(stored); + when(mainStoreRepo.save(stored)).thenReturn(stored); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(stored); + when(mainStoreRepo.findByProviderServiceMapIDOrNullOrderByFacilityName(PSM_ID)).thenReturn(asList); + when(mainStoreRepo.saveAll(anyList())).thenReturn(asList); + when(mainStoreRepo.getAllMainFacility(PSM_ID, true)).thenReturn(facilities); + when(mainStoreRepo.getAllMainFacility(PSM_ID, true, FACILITY_ID)).thenReturn(facilities); + when(mainStoreRepo.getChildFacility(PSM_ID, FACILITY_ID)).thenReturn(facilities); + when(mainStoreRepo.findByBlockIDAndDeletedFalseOrderByFacilityName(BLOCK_ID)).thenReturn(facilities); + when(mainStoreRepo.findByBlockIDOrderByFacilityName(BLOCK_ID)).thenReturn(facilities); + when(mainStoreRepo.findByParentFacilityIDAndDeletedFalseOrderByFacilityName(FACILITY_ID)) + .thenReturn(facilities); + + assertSame(stored, service.createMainStore(stored)); + assertSame(stored, service.getMainStore(FACILITY_ID)); + assertSame(asList, service.getAllMainStore(PSM_ID)); + assertSame(asList, service.addAllMainStore(new ArrayList<>())); + assertSame(facilities, service.getMainFacility(PSM_ID, true)); + assertSame(facilities, service.getMainFacility(PSM_ID, true, FACILITY_ID)); + assertSame(facilities, service.getChildFacility(PSM_ID, FACILITY_ID)); + assertSame(facilities, service.getFacilitiesByBlock(BLOCK_ID)); + assertSame(facilities, service.getAllFacilitiesByBlock(BLOCK_ID)); + assertSame(facilities, service.getChildFacilitiesByParent(FACILITY_ID)); + } + + @Test + @DisplayName("getFacilitiesByBlockAndLevel should ignore the rural-urban split when none is asked for") + void getFacilitiesByBlockAndLevel_shouldIgnoreSplitWhenNotAsked() { + when(mainStoreRepo.findByBlockIDAndLevelValue(BLOCK_ID, 4)).thenReturn(new ArrayList<>()); + + service.getFacilitiesByBlockAndLevel(BLOCK_ID, 4, null); + service.getFacilitiesByBlockAndLevel(BLOCK_ID, 4, ""); + + verify(mainStoreRepo, org.mockito.Mockito.times(2)).findByBlockIDAndLevelValue(BLOCK_ID, 4); + } + + @Test + @DisplayName("getFacilitiesByBlockAndLevel should narrow by the rural-urban split when asked for") + void getFacilitiesByBlockAndLevel_shouldNarrowBySplit() { + when(mainStoreRepo.findByBlockIDAndFacilityLevel(BLOCK_ID, 4, "Rural")).thenReturn(new ArrayList<>()); + + service.getFacilitiesByBlockAndLevel(BLOCK_ID, 4, "Rural"); + + verify(mainStoreRepo).findByBlockIDAndFacilityLevel(BLOCK_ID, 4, "Rural"); + } + + @Test + @DisplayName("getMapStore should answer the mapped facilities of the provider") + void getMapStore_shouldAnswerMappedFacilities() { + V_FetchFacility request = new V_FetchFacility(); + request.setProviderServiceMapID(PSM_ID); + List stored = List.of(request); + when(fetchFacilityRepo.findByProviderServiceMapID(PSM_ID)).thenReturn(stored); + + assertSame(stored, service.getMapStore(request)); + } + + @Test + @DisplayName("checkStoreCode should report a facility code the provider already uses") + void checkStoreCode_shouldReportUsedCode() { + M_Facility request = facility(null, "PHC North"); + request.setFacilityCode("PHC-1"); + when(mainStoreRepo.findByFacilityCodeAndProviderServiceMapID("PHC-1", PSM_ID)) + .thenReturn(List.of(facility(FACILITY_ID, "PHC North"))); + + assertTrue(service.checkStoreCode(request)); + } + + @Test + @DisplayName("checkStoreCode should clear a facility code nobody uses yet") + void checkStoreCode_shouldClearFreeCode() { + M_Facility request = facility(null, "PHC North"); + request.setFacilityCode("PHC-2"); + when(mainStoreRepo.findByFacilityCodeAndProviderServiceMapID("PHC-2", PSM_ID)) + .thenReturn(new ArrayList<>()); + + assertFalse(service.checkStoreCode(request)); + } + + @Test + @DisplayName("getMappedVillageIDs should hand back what the repository holds") + void getMappedVillageIDs_shouldHandBackRepositoryContents() { + List stored = List.of(601, 602); + when(facilityVillageMappingRepo.findMappedVillageIDsByBlockID(BLOCK_ID)).thenReturn(stored); + + assertSame(stored, service.getMappedVillageIDs(BLOCK_ID)); + } + + @Test + @DisplayName("getVillageMappingsByFacility should answer the villages a live facility serves") + void getVillageMappingsByFacility_shouldAnswerServedVillages() { + ArrayList stored = new ArrayList<>(List.of(new FacilityVillageMapping())); + when(mainStoreRepo.findByFacilityIDAndDeleted(FACILITY_ID, false)) + .thenReturn(facility(FACILITY_ID, "PHC North")); + when(facilityVillageMappingRepo.findByFacilityIDAndDeletedFalse(FACILITY_ID)).thenReturn(stored); + + assertSame(stored, service.getVillageMappingsByFacility(FACILITY_ID)); + } + + @Test + @DisplayName("getVillageMappingsByFacility should answer nothing for a facility that is retired") + void getVillageMappingsByFacility_shouldAnswerNothingForRetiredFacility() { + when(mainStoreRepo.findByFacilityIDAndDeleted(FACILITY_ID, false)).thenReturn(null); + + assertTrue(service.getVillageMappingsByFacility(FACILITY_ID).isEmpty()); + verify(facilityVillageMappingRepo, never()).findByFacilityIDAndDeletedFalse(anyInt()); + } + } + + @Nested + @DisplayName("deleteStore") + class DeleteStoreTests { + + @Test + @DisplayName("should retire a facility that nothing else hangs off") + void deleteStore_shouldRetireUnusedFacility() throws Exception { + M_Facility stored = facility(FACILITY_ID, "PHC North"); + M_Facility request = facility(FACILITY_ID, "PHC North"); + request.setDeleted(Boolean.TRUE); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(stored); + when(mainStoreRepo.findByMainFacilityIDAndDeletedOrderByFacilityName(FACILITY_ID, false)) + .thenReturn(new ArrayList<>()); + when(parkingPlaceRepository.findByFacilityIDAndDeleted(FACILITY_ID, false)).thenReturn(new ArrayList<>()); + when(vanMasterRepository.findByFacilityIDAndDeleted(FACILITY_ID, false)).thenReturn(new ArrayList<>()); + when(mainStoreRepo.save(stored)).thenReturn(stored); + + assertTrue(service.deleteStore(request).getDeleted()); + } + + @Test + @DisplayName("should refuse to retire a facility that still has live children") + void deleteStore_shouldRefuseWhenChildrenAreLive() { + M_Facility request = facility(FACILITY_ID, "PHC North"); + request.setDeleted(Boolean.TRUE); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(facility(FACILITY_ID, "PHC North")); + when(mainStoreRepo.findByMainFacilityIDAndDeletedOrderByFacilityName(FACILITY_ID, false)) + .thenReturn(new ArrayList<>(List.of(facility(502, "Sub Centre")))); + + Exception thrown = assertThrows(Exception.class, () -> service.deleteStore(request)); + assertEquals("Child Stores are still active", thrown.getMessage()); + } + + @Test + @DisplayName("should refuse to retire a facility a parking place still points at") + void deleteStore_shouldRefuseWhenMappedToParkingPlace() { + M_Facility request = facility(FACILITY_ID, "PHC North"); + request.setDeleted(Boolean.TRUE); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(facility(FACILITY_ID, "PHC North")); + when(mainStoreRepo.findByMainFacilityIDAndDeletedOrderByFacilityName(FACILITY_ID, false)) + .thenReturn(new ArrayList<>()); + when(parkingPlaceRepository.findByFacilityIDAndDeleted(FACILITY_ID, false)) + .thenReturn(List.of(new M_Parkingplace())); + + Exception thrown = assertThrows(Exception.class, () -> service.deleteStore(request)); + assertEquals("Store mapped to parking place", thrown.getMessage()); + } + + @Test + @DisplayName("should refuse to retire a facility a van still points at") + void deleteStore_shouldRefuseWhenMappedToVan() { + M_Facility request = facility(FACILITY_ID, "PHC North"); + request.setDeleted(Boolean.TRUE); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(facility(FACILITY_ID, "PHC North")); + when(mainStoreRepo.findByMainFacilityIDAndDeletedOrderByFacilityName(FACILITY_ID, false)) + .thenReturn(new ArrayList<>()); + when(parkingPlaceRepository.findByFacilityIDAndDeleted(FACILITY_ID, false)).thenReturn(new ArrayList<>()); + when(vanMasterRepository.findByFacilityIDAndDeleted(FACILITY_ID, false)) + .thenReturn(List.of(new M_Van())); + + Exception thrown = assertThrows(Exception.class, () -> service.deleteStore(request)); + assertEquals("Store mapped to van", thrown.getMessage()); + } + + @Test + @DisplayName("should reinstate a top-level facility without further checks") + void deleteStore_shouldReinstateTopLevelFacility() throws Exception { + M_Facility stored = facility(FACILITY_ID, "PHC North"); + stored.setDeleted(Boolean.TRUE); + M_Facility request = facility(FACILITY_ID, "PHC North"); + request.setDeleted(Boolean.FALSE); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(stored); + when(mainStoreRepo.save(stored)).thenReturn(stored); + + assertFalse(service.deleteStore(request).getDeleted()); + } + + @Test + @DisplayName("should reinstate a child facility once its parent is live again") + void deleteStore_shouldReinstateChildUnderLiveParent() throws Exception { + M_Facility stored = facility(FACILITY_ID, "Sub Centre"); + stored.setMainFacilityID(500); + M_Facility request = facility(FACILITY_ID, "Sub Centre"); + request.setDeleted(Boolean.FALSE); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(stored); + when(mainStoreRepo.findByFacilityIDAndDeleted(500, false)).thenReturn(facility(500, "PHC North")); + when(mainStoreRepo.save(stored)).thenReturn(stored); + + assertFalse(service.deleteStore(request).getDeleted()); + } + + @Test + @DisplayName("should refuse to reinstate a child whose parent is still retired") + void deleteStore_shouldRefuseReinstatingUnderRetiredParent() { + M_Facility stored = facility(FACILITY_ID, "Sub Centre"); + stored.setMainFacilityID(500); + M_Facility request = facility(FACILITY_ID, "Sub Centre"); + request.setDeleted(Boolean.FALSE); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(stored); + when(mainStoreRepo.findByFacilityIDAndDeleted(500, false)).thenReturn(null); + + Exception thrown = assertThrows(Exception.class, () -> service.deleteStore(request)); + assertEquals("Parent Stores are still inactive", thrown.getMessage()); + } + + @Test + @DisplayName("should refuse a facility that is not on record") + void deleteStore_shouldRefuseUnknownFacility() { + M_Facility request = facility(FACILITY_ID, "PHC North"); + request.setDeleted(Boolean.TRUE); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(null); + + Exception thrown = assertThrows(Exception.class, () -> service.deleteStore(request)); + assertEquals("No store available", thrown.getMessage()); + } + } + + @Nested + @DisplayName("mapStore and deleteMapStore") + class MappingTests { + + private M_facilityMap mapping(boolean isMain) { + M_facilityMap mapping = new M_facilityMap(); + mapping.setFacilityID(FACILITY_ID); + mapping.setIsMainFacility(isMain); + mapping.setCreatedBy("admin"); + return mapping; + } + + @Test + @DisplayName("mapStore should free the previous parking place before claiming the new one") + void mapStore_shouldFreePreviousParkingPlace() { + M_facilityMap request = mapping(true); + request.setParkingPlaceID(701); + request.setOldParkingPlaceID(700); + when(parkingPlaceRepository.updatePPMap(anyInt(), any(), anyString(), any())).thenReturn(1); + + assertEquals(1, service.mapStore(List.of(request))); + verify(parkingPlaceRepository).updatePPMap(700, null, "admin", null); + verify(parkingPlaceRepository).updatePPMap(701, FACILITY_ID, "admin", true); + } + + @Test + @DisplayName("mapStore should free the previous van before claiming the new one") + void mapStore_shouldFreePreviousVan() { + M_facilityMap request = mapping(false); + request.setVanID(801); + request.setOldVanID(800); + when(vanMasterRepository.updateVanMap(anyInt(), any(), anyString(), any())).thenReturn(1); + + assertEquals(1, service.mapStore(List.of(request))); + verify(vanMasterRepository).updateVanMap(800, null, "admin", null); + verify(vanMasterRepository).updateVanMap(801, FACILITY_ID, "admin", true); + } + + @Test + @DisplayName("mapStore should leave a main facility alone when no parking place is named") + void mapStore_shouldLeaveMainFacilityAloneWithoutParkingPlace() { + assertEquals(0, service.mapStore(List.of(mapping(true)))); + verify(parkingPlaceRepository, never()).updatePPMap(anyInt(), any(), anyString(), any()); + } + + @Test + @DisplayName("deleteMapStore should free the parking place once no van hangs off it") + void deleteMapStore_shouldFreeParkingPlace() throws Exception { + M_facilityMap request = mapping(true); + request.setParkingPlaceID(701); + when(vanMasterRepository.findByParkingPlaceIDAndFacilityIDIsNotNull(701)).thenReturn(new ArrayList<>()); + when(parkingPlaceRepository.updatePPMap(701, null, "admin", null)).thenReturn(1); + + assertEquals(1, service.deleteMapStore(request)); + } + + @Test + @DisplayName("deleteMapStore should refuse a parking place that still has a mapped van") + void deleteMapStore_shouldRefuseParkingPlaceWithMappedVan() { + M_facilityMap request = mapping(true); + request.setParkingPlaceID(701); + when(vanMasterRepository.findByParkingPlaceIDAndFacilityIDIsNotNull(701)) + .thenReturn(List.of(new M_Van())); + + Exception thrown = assertThrows(Exception.class, () -> service.deleteMapStore(request)); + assertEquals("Please Unmap van under this Parking Place", thrown.getMessage()); + } + + @Test + @DisplayName("deleteMapStore should free the van when no parking place is named") + void deleteMapStore_shouldFreeVan() throws Exception { + M_facilityMap request = mapping(false); + request.setVanID(801); + when(vanMasterRepository.updateVanMap(801, null, "admin", null)).thenReturn(1); + + assertEquals(1, service.deleteMapStore(request)); + } + } + + @Nested + @DisplayName("createFacilityWithHierarchy") + class CreateHierarchyTests { + + @Test + @DisplayName("should refuse a facility whose name is already taken in the block") + void create_shouldRefuseDuplicateNameInBlock() { + when(mainStoreRepo.existsByFacilityNameAndBlockIDAndDeletedFalse("PHC North", BLOCK_ID)) + .thenReturn(true); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.createFacilityWithHierarchy(facility(null, "PHC North"), null, null, null)); + assertEquals("Facility with this name already exists in this block", thrown.getMessage()); + } + + @Test + @DisplayName("should refuse a child that does not sit one level below the new facility") + void create_shouldRefuseChildAtWrongLevel() { + M_Facility request = facility(null, "PHC North"); + request.setFacilityTypeID(3); + M_Facility child = facility(502, "Sub Centre"); + child.setFacilityTypeID(9); + when(facilityTypeRepo.findByFacilityTypeID(3)).thenReturn(facilityType(3, 2)); + when(facilityTypeRepo.findByFacilityTypeID(9)).thenReturn(facilityType(9, 5)); + when(mainStoreRepo.findByFacilityIDAndDeleted(502, false)).thenReturn(child); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.createFacilityWithHierarchy(request, null, null, List.of(502))); + assertTrue(thrown.getMessage().contains("Hierarchy level mismatch"), thrown.getMessage()); + } + + @Test + @DisplayName("should attach the villages the new facility serves") + void create_shouldAttachServedVillages() { + M_Facility request = facility(null, "PHC North"); + request.setCreatedBy("admin"); + M_Facility saved = facility(FACILITY_ID, "PHC North"); + when(mainStoreRepo.save(request)).thenReturn(saved); + when(facilityVillageMappingRepo + .findByFacilityIDAndDistrictBranchIDAndDeletedTrue(FACILITY_ID, 601)).thenReturn(null); + + service.createFacilityWithHierarchy(request, List.of(601), 601, null); + + verify(facilityVillageMappingRepo).save(any(FacilityVillageMapping.class)); + assertEquals(601, request.getMainVillageID()); + } + + @Test + @DisplayName("should reinstate a village mapping that was previously retired") + void create_shouldReinstateRetiredVillageMapping() { + M_Facility request = facility(null, "PHC North"); + request.setCreatedBy("admin"); + M_Facility saved = facility(FACILITY_ID, "PHC North"); + FacilityVillageMapping retired = new FacilityVillageMapping(); + retired.setDeleted(Boolean.TRUE); + when(mainStoreRepo.save(request)).thenReturn(saved); + when(facilityVillageMappingRepo + .findByFacilityIDAndDistrictBranchIDAndDeletedTrue(FACILITY_ID, 601)).thenReturn(retired); + + service.createFacilityWithHierarchy(request, List.of(601), 601, null); + + assertFalse(retired.getDeleted()); + assertEquals("admin", retired.getModifiedBy()); + } + + @Test + @DisplayName("should re-parent the children the new facility takes over") + void create_shouldReparentChildren() { + M_Facility request = facility(null, "PHC North"); + request.setCreatedBy("admin"); + M_Facility saved = facility(FACILITY_ID, "PHC North"); + M_Facility child = facility(502, "Sub Centre"); + child.setProviderServiceMapID(null); + child.setIsMainFacility(Boolean.TRUE); + when(mainStoreRepo.save(request)).thenReturn(saved); + when(mainStoreRepo.findByFacilityID(502)).thenReturn(child); + + service.createFacilityWithHierarchy(request, null, null, List.of(502)); + + assertEquals(FACILITY_ID, child.getParentFacilityID()); + verify(mainStoreRepo).updateStoreFields(502, false, FACILITY_ID, "SUB"); + } + } + + @Nested + @DisplayName("deleteFacilityWithHierarchy") + class DeleteHierarchyTests { + + @Test + @DisplayName("should release the children before retiring the facility") + void delete_shouldReleaseChildrenFirst() throws Exception { + M_Facility stored = facility(FACILITY_ID, "PHC North"); + M_Facility child = facility(502, "Sub Centre"); + child.setParentFacilityID(FACILITY_ID); + child.setProviderServiceMapID(null); + FacilityVillageMapping villageMapping = new FacilityVillageMapping(); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(stored); + when(mainStoreRepo.findByParentFacilityIDAndDeletedFalseOrderByFacilityName(FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(child))); + when(mainStoreRepo.save(stored)).thenReturn(stored); + when(facilityVillageMappingRepo.findByFacilityIDAndDeletedFalse(FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(villageMapping))); + + M_Facility retired = service.deleteFacilityWithHierarchy(FACILITY_ID, "admin"); + + assertTrue(retired.getDeleted()); + assertNull(child.getParentFacilityID(), "a released child must not point at a retired parent"); + verify(mainStoreRepo).updateStoreFields(502, true, null, "MAIN"); + verify(ashaSupervisorMappingService).cascadeDeleteByFacilityID(FACILITY_ID, "admin"); + assertTrue(villageMapping.getDeleted(), "the villages it served must be released too"); + } + + @Test + @DisplayName("should refuse a facility that is not on record") + void delete_shouldRefuseUnknownFacility() { + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(null); + + Exception thrown = assertThrows(Exception.class, + () -> service.deleteFacilityWithHierarchy(FACILITY_ID, "admin")); + assertEquals("Facility not found", thrown.getMessage()); + } + } + + @Nested + @DisplayName("updateFacilityWithHierarchy") + class UpdateHierarchyTests { + + @Test + @DisplayName("should refuse a facility that is not on record") + void update_shouldRefuseUnknownFacility() { + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(null); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.updateFacilityWithHierarchy(facility(FACILITY_ID, "PHC North"), null, null, null)); + assertEquals("Facility not found", thrown.getMessage()); + } + + @Test + @DisplayName("should refuse a rename onto a name another facility in the block already uses") + void update_shouldRefuseDuplicateNameInBlock() { + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(facility(FACILITY_ID, "old name")); + when(mainStoreRepo.existsByFacilityNameAndBlockIDAndNotFacilityID("PHC North", BLOCK_ID, FACILITY_ID)) + .thenReturn(true); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.updateFacilityWithHierarchy(facility(FACILITY_ID, "PHC North"), null, null, null)); + assertEquals("Facility with this name already exists in this block", thrown.getMessage()); + } + + @Test + @DisplayName("should copy only the fields the request actually sets") + void update_shouldCopyOnlySuppliedFields() { + M_Facility existing = facility(FACILITY_ID, "old name"); + existing.setRuralUrban("Rural"); + M_Facility request = new M_Facility(); + request.setFacilityID(FACILITY_ID); + request.setFacilityName("PHC North"); + request.setModifiedBy("admin"); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(existing); + when(mainStoreRepo.save(existing)).thenReturn(existing); + + service.updateFacilityWithHierarchy(request, null, 601, null); + + assertEquals("PHC North", existing.getFacilityName()); + assertEquals("Rural", existing.getRuralUrban(), "an unset field keeps the value on record"); + assertEquals(601, existing.getMainVillageID()); + } + + @Test + @DisplayName("should release the villages the facility no longer serves") + void update_shouldReleaseDroppedVillages() { + M_Facility existing = facility(FACILITY_ID, "PHC North"); + M_Facility request = facility(FACILITY_ID, "PHC North"); + request.setModifiedBy("admin"); + FacilityVillageMapping dropped = new FacilityVillageMapping(); + dropped.setDistrictBranchID(602); + FacilityVillageMapping kept = new FacilityVillageMapping(); + kept.setDistrictBranchID(601); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(existing); + when(mainStoreRepo.save(existing)).thenReturn(existing); + when(facilityVillageMappingRepo.findByFacilityIDAndDeletedFalse(FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(dropped, kept))); + + service.updateFacilityWithHierarchy(request, List.of(601), 601, null); + + assertTrue(dropped.getDeleted(), "a village dropped from the list must be released"); + assertFalse(Boolean.TRUE.equals(kept.getDeleted())); + } + + @Test + @DisplayName("should refuse a child that does not sit one level below the facility") + void update_shouldRefuseChildAtWrongLevel() { + M_Facility existing = facility(FACILITY_ID, "PHC North"); + existing.setFacilityTypeID(3); + M_Facility request = facility(FACILITY_ID, "PHC North"); + request.setModifiedBy("admin"); + M_Facility child = facility(502, "Sub Centre"); + child.setFacilityTypeID(9); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(existing); + when(mainStoreRepo.save(existing)).thenReturn(existing); + when(facilityTypeRepo.findByFacilityTypeID(3)).thenReturn(facilityType(3, 2)); + when(facilityTypeRepo.findByFacilityTypeID(9)).thenReturn(facilityType(9, 5)); + when(mainStoreRepo.findByFacilityIDAndDeleted(502, false)).thenReturn(child); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.updateFacilityWithHierarchy(request, null, null, List.of(502))); + assertTrue(thrown.getMessage().contains("Hierarchy level mismatch"), thrown.getMessage()); + } + + @Test + @DisplayName("should promote a child that is no longer part of the hierarchy") + void update_shouldPromoteReleasedChild() { + M_Facility existing = facility(FACILITY_ID, "PHC North"); + M_Facility request = facility(FACILITY_ID, "PHC North"); + request.setModifiedBy("admin"); + M_Facility released = facility(503, "Sub Centre B"); + released.setProviderServiceMapID(null); + when(mainStoreRepo.findByFacilityID(FACILITY_ID)).thenReturn(existing); + when(mainStoreRepo.save(existing)).thenReturn(existing); + when(mainStoreRepo.findByParentFacilityIDAndDeletedFalseOrderByFacilityName(FACILITY_ID)) + .thenReturn(new ArrayList<>(List.of(released))); + + service.updateFacilityWithHierarchy(request, null, null, new ArrayList<>()); + + verify(mainStoreRepo).clearParentFacilityID(FACILITY_ID, "admin"); + verify(mainStoreRepo).updateStoreFields(503, true, null, "MAIN"); + } + } +} diff --git a/src/test/java/com/iemr/admin/service/supplier/SupplierServiceImplTest.java b/src/test/java/com/iemr/admin/service/supplier/SupplierServiceImplTest.java new file mode 100644 index 0000000..e3aedce --- /dev/null +++ b/src/test/java/com/iemr/admin/service/supplier/SupplierServiceImplTest.java @@ -0,0 +1,113 @@ +/* +* 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.admin.service.supplier; + +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.admin.data.supplier.M_Supplier; +import com.iemr.admin.repo.supplier.SupplierRepo; + +import static org.junit.jupiter.api.Assertions.assertFalse; +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.anyList; +import static org.mockito.Mockito.when; + +/** The supplier service is thin over its repository, but it answers nothing rather than an empty batch. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("SupplierServiceImpl Test Suite") +class SupplierServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer RECORD_ID = 11; + + @Mock + private SupplierRepo supplierRepo; + + @InjectMocks + private SupplierServiceImpl service; + + @Test + @DisplayName("createSupplier should answer the records it stored") + void create_shouldAnswerStoredRecords() { + ArrayList stored = new ArrayList<>(List.of(new M_Supplier())); + when(supplierRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.createSupplier(new ArrayList<>())); + } + + @Test + @DisplayName("createSupplier should answer nothing when the store took nothing") + void create_shouldAnswerNothingWhenNothingStored() { + when(supplierRepo.saveAll(anyList())).thenReturn(new ArrayList()); + + assertNull(service.createSupplier(new ArrayList<>())); + } + + @Test + @DisplayName("the lookups should each reach their own repository query") + void lookups_shouldReachTheirOwnQuery() { + M_Supplier record = new M_Supplier(); + ArrayList records = new ArrayList<>(List.of(record)); + when(supplierRepo.getSupplierData(PSM_ID)).thenReturn(records); + when(supplierRepo.geteditedData(RECORD_ID)).thenReturn(record); + when(supplierRepo.save(record)).thenReturn(record); + + assertSame(records, service.getSupplier(PSM_ID)); + assertSame(record, service.editSupplier(RECORD_ID)); + assertSame(record, service.saveEditedData(record)); + } + + @Test + @DisplayName("checkSupplierCode should report a code the provider already uses") + void check_shouldReportUsedCode() { + M_Supplier request = new M_Supplier(); + request.setSupplierCode("C-1"); + request.setProviderServiceMapID(PSM_ID); + when(supplierRepo.findBySupplierCodeAndProviderServiceMapID("C-1", PSM_ID)).thenReturn(List.of(new M_Supplier())); + + assertTrue(service.checkSupplierCode(request)); + } + + @Test + @DisplayName("checkSupplierCode should clear a code nobody uses yet") + void check_shouldClearFreeCode() { + M_Supplier request = new M_Supplier(); + request.setSupplierCode("C-2"); + request.setProviderServiceMapID(PSM_ID); + when(supplierRepo.findBySupplierCodeAndProviderServiceMapID("C-2", PSM_ID)).thenReturn(new ArrayList()); + + assertFalse(service.checkSupplierCode(request)); + } +} diff --git a/src/test/java/com/iemr/admin/service/telemedicine/TMServiceImplTest.java b/src/test/java/com/iemr/admin/service/telemedicine/TMServiceImplTest.java new file mode 100644 index 0000000..d8b5068 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/telemedicine/TMServiceImplTest.java @@ -0,0 +1,172 @@ +/* +* 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.admin.service.telemedicine; + +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.admin.data.telemedicine.M_UserTemp; +import com.iemr.admin.data.telemedicine.Specialization; +import com.iemr.admin.data.telemedicine.TMinput; +import com.iemr.admin.data.telemedicine.UserSpecializationMapping; +import com.iemr.admin.repo.telemedicine.SpecializationRepo; +import com.iemr.admin.repo.telemedicine.UserRepo; +import com.iemr.admin.repo.telemedicine.UserSpecializationMappingRepo; + +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.anyList; +import static org.mockito.Mockito.when; + +/** + * The telemedicine service rebuilds the specialist roster out of the wide user + * row the reporting query answers, and records which specialities each holds. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("TMServiceImpl Test Suite") +class TMServiceImplTest { + + private static final Integer PROVIDER_ID = 5; + private static final String SCREEN = "TM"; + + @Mock + private UserRepo userRepo; + + @Mock + private SpecializationRepo specializationRepo; + + @Mock + private UserSpecializationMappingRepo userSpecializationMappingRepo; + + @InjectMocks + private TMServiceImpl service; + + private static TMinput request() { + TMinput input = new TMinput(); + input.setServiceproviderID(PROVIDER_ID); + input.setScreenName(SCREEN); + return input; + } + + /** The reporting query answers the whole user row; only some columns are read. */ + private static Object[] userRow() { + Object[] row = new Object[33]; + row[0] = 3117; + row[2] = "Asha"; + row[4] = "Rao"; + row[12] = 7; + row[14] = "asha.rao"; + row[22] = "asha.rao@example.org"; + row[24] = (short) 5; + row[32] = false; + return row; + } + + @Test + @DisplayName("getUser should rebuild one specialist per row the query answers") + void getUser_shouldRebuildEachRow() { + when(userRepo.getUserTM(PROVIDER_ID, SCREEN)).thenReturn(new ArrayList<>(List.of(userRow()))); + + ArrayList users = service.getUser(request()); + + assertEquals(1, users.size()); + M_UserTemp user = users.get(0); + assertEquals(3117L, user.getUserID()); + assertEquals("Asha", user.getFirstName()); + assertEquals("Rao", user.getLastName()); + assertEquals("asha.rao", user.getUserName()); + assertEquals("asha.rao@example.org", user.getEmailID()); + assertEquals(7, user.getDesignationID()); + assertEquals(PROVIDER_ID, user.getServiceProviderID()); + assertEquals(false, user.getDeleted()); + assertNull(user.getDesignation(), "the designation record must not travel with the roster row"); + } + + @Test + @DisplayName("getUser should answer nothing when the provider has no specialist on that screen") + void getUser_shouldAnswerNothingForProviderWithoutSpecialists() { + when(userRepo.getUserTM(PROVIDER_ID, SCREEN)).thenReturn(new ArrayList<>()); + + assertTrue(service.getUser(request()).isEmpty()); + } + + @Test + @DisplayName("getSpecialization should answer only the specialities still in use") + void getSpecialization_shouldAnswerLiveSpecialities() { + ArrayList live = new ArrayList<>(List.of(new Specialization())); + when(specializationRepo.findByDeleted(false)).thenReturn(live); + + assertSame(live, service.getSpecialization()); + } + + @Test + @DisplayName("getUserSpecialization should answer the specialities held under the provider asked for") + void getUserSpecialization_shouldAnswerSpecialitiesOfProvider() { + ArrayList held = new ArrayList<>(List.of(new UserSpecializationMapping())); + when(userSpecializationMappingRepo.findByServiceprovider(PROVIDER_ID)).thenReturn(held); + + assertSame(held, service.getUserSpecialization(PROVIDER_ID)); + } + + @Test + @DisplayName("saveUserSpecialization should answer the specialities the repository stored") + void saveUserSpecialization_shouldAnswerStoredSpecialities() { + ArrayList stored = new ArrayList<>(List.of(new UserSpecializationMapping())); + when(userSpecializationMappingRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.saveUserSpecialization(new ArrayList<>())); + } + + @Test + @DisplayName("findUserSpecialization and saveoneUserSpecialization should each reach their own query") + void singleRecordOperations_shouldReachTheirOwnQuery() { + UserSpecializationMapping stored = new UserSpecializationMapping(); + stored.setUserSpecializationMapID(8001); + when(userSpecializationMappingRepo.findByUserSpecializationMapID(8001)).thenReturn(stored); + when(userSpecializationMappingRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.findUserSpecialization(stored)); + assertSame(stored, service.saveoneUserSpecialization(stored)); + } + + @Test + @DisplayName("findUserSpecialization should answer nothing when the speciality is unknown") + void findUserSpecialization_shouldAnswerNothingForUnknownSpeciality() { + UserSpecializationMapping request = new UserSpecializationMapping(); + request.setUserSpecializationMapID(-1); + when(userSpecializationMappingRepo.findByUserSpecializationMapID(-1)).thenReturn(null); + + assertNull(service.findUserSpecialization(request)); + } +} diff --git a/src/test/java/com/iemr/admin/service/telemedicine/VideoConsultationAPIServiceImplTest.java b/src/test/java/com/iemr/admin/service/telemedicine/VideoConsultationAPIServiceImplTest.java new file mode 100644 index 0000000..ed4cec7 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/telemedicine/VideoConsultationAPIServiceImplTest.java @@ -0,0 +1,196 @@ +/* +* 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.admin.service.telemedicine; + +import java.util.HashMap; + +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.admin.utils.exception.VideoConsultationException; +import com.iemr.admin.utils.http.HttpUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * The API service is the only thing that talks to the external conferencing + * platform, so it is checked against a stand-in for that platform's replies. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VideoConsultationAPIServiceImpl Test Suite") +class VideoConsultationAPIServiceImplTest { + + private static final String BASE_URL = "https://conferencing.example.org"; + private static final Long REMOTE_ID = 990011L; + + @Mock + private HttpUtils httpUtils; + + private HttpUtils originalHttpUtils; + + private VideoConsultationAPIServiceImpl service; + + @BeforeEach + @DisplayName("Stand in for the external platform and point the service at a stub address") + void setUp() { + originalHttpUtils = (HttpUtils) ReflectionTestUtils.getField(VideoConsultationAPIServiceImpl.class, + "httpUtils"); + ReflectionTestUtils.setField(VideoConsultationAPIServiceImpl.class, "httpUtils", httpUtils); + service = new VideoConsultationAPIServiceImpl(); + ReflectionTestUtils.setField(service, "videoConsultationAuth", "api-key"); + ReflectionTestUtils.setField(service, "videoConsultationBaseUrl", BASE_URL); + ReflectionTestUtils.setField(service, "videoConsultationCreateUser", + "videoConsultation-base-url/api/users"); + ReflectionTestUtils.setField(service, "videoConsultationEditUser", "videoConsultation-base-url/api/users"); + } + + @AfterEach + @DisplayName("Put the real platform client back so no other suite sees the stand-in") + void tearDown() { + ReflectionTestUtils.setField(VideoConsultationAPIServiceImpl.class, "httpUtils", originalHttpUtils); + } + + private static HashMap account() { + HashMap obj = new HashMap<>(); + obj.put("name", "Asha"); + obj.put("email", "asha.rao@example.org"); + return obj; + } + + @Test + @DisplayName("createUser should answer the id the platform gave the new account") + void createUser_shouldAnswerRemoteId() throws Exception { + when(httpUtils.post(anyString(), anyString(), any())) + .thenReturn("{\"userid\":990011,\"result\":\"ok\"}"); + + assertEquals(REMOTE_ID, service.createUser(account())); + } + + @Test + @DisplayName("createUser should send the account to the address the platform is configured at") + void createUser_shouldSendToConfiguredAddress() throws Exception { + when(httpUtils.post(anyString(), anyString(), any())) + .thenReturn("{\"userid\":990011,\"result\":\"ok\"}"); + ArgumentCaptor url = ArgumentCaptor.forClass(String.class); + ArgumentCaptor body = ArgumentCaptor.forClass(String.class); + ArgumentCaptor> header = ArgumentCaptor.forClass(HashMap.class); + + service.createUser(account()); + + org.mockito.Mockito.verify(httpUtils).post(url.capture(), body.capture(), header.capture()); + assertEquals(BASE_URL + "/api/users", url.getValue()); + assertTrue(body.getValue().contains("asha.rao@example.org"), body.getValue()); + assertEquals("api-key", header.getValue().get("X-APIkey-Header")); + assertEquals("application/json", header.getValue().get("Content-Type")); + } + + @Test + @DisplayName("createUser should report the platform's own reason when it refuses the account") + void createUser_shouldReportRemoteRefusal() { + when(httpUtils.post(anyString(), anyString(), any())) + .thenReturn("{\"userid\":0,\"result\":\"email already registered\"}"); + + VideoConsultationException refusal = assertThrows(VideoConsultationException.class, + () -> service.createUser(account())); + + assertEquals("email already registered", refusal.getMessage()); + } + + @Test + @DisplayName("createUser should give up when the platform answers something that is not an account") + void createUser_shouldGiveUpOnUnreadableReply() { + when(httpUtils.post(anyString(), anyString(), any())).thenReturn("service unavailable"); + + assertThrows(RuntimeException.class, () -> service.createUser(account())); + } + + @Test + @DisplayName("editUser should answer the id the platform confirmed") + void editUser_shouldAnswerConfirmedId() throws Exception { + when(httpUtils.put(anyString(), anyString(), any())) + .thenReturn("{\"userid\":990011,\"result\":\"ok\"}"); + + assertEquals(REMOTE_ID, service.editUser(account(), REMOTE_ID, "psmri")); + } + + @Test + @DisplayName("editUser should address the account and domain being changed") + void editUser_shouldAddressAccountAndDomain() throws Exception { + when(httpUtils.put(anyString(), anyString(), any())) + .thenReturn("{\"userid\":990011,\"result\":\"ok\"}"); + ArgumentCaptor url = ArgumentCaptor.forClass(String.class); + + service.editUser(account(), REMOTE_ID, "psmri"); + + org.mockito.Mockito.verify(httpUtils).put(url.capture(), anyString(), any()); + assertEquals(BASE_URL + "/api/users/990011/psmri", url.getValue()); + } + + @Test + @DisplayName("editUser should report the platform's own reason when it refuses the change") + void editUser_shouldReportRemoteRefusal() { + when(httpUtils.put(anyString(), anyString(), any())) + .thenReturn("{\"userid\":0,\"result\":\"account is locked\"}"); + + VideoConsultationException refusal = assertThrows(VideoConsultationException.class, + () -> service.editUser(account(), REMOTE_ID, "psmri")); + + assertEquals("account is locked", refusal.getMessage()); + } + + @Test + @DisplayName("editUser should give up when the platform answers something that is not an account") + void editUser_shouldGiveUpOnUnreadableReply() { + when(httpUtils.put(anyString(), anyString(), any())).thenReturn("gateway timeout"); + + assertThrows(RuntimeException.class, () -> service.editUser(account(), REMOTE_ID, "psmri")); + } + + @Test + @DisplayName("createUser should still reach the platform when no API key is configured") + void createUser_shouldReachPlatformWithoutApiKey() throws Exception { + ReflectionTestUtils.setField(service, "videoConsultationAuth", null); + when(httpUtils.post(anyString(), anyString(), any())) + .thenReturn("{\"userid\":990011,\"result\":\"ok\"}"); + ArgumentCaptor> header = ArgumentCaptor.forClass(HashMap.class); + + service.createUser(account()); + + org.mockito.Mockito.verify(httpUtils).post(anyString(), anyString(), header.capture()); + assertTrue(header.getValue().get("X-APIkey-Header") == null, "no key must be sent when none is configured"); + } +} diff --git a/src/test/java/com/iemr/admin/service/telemedicine/VideoConsultationServiceImplTest.java b/src/test/java/com/iemr/admin/service/telemedicine/VideoConsultationServiceImplTest.java new file mode 100644 index 0000000..0ebb228 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/telemedicine/VideoConsultationServiceImplTest.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.admin.service.telemedicine; + +import java.util.ArrayList; +import java.util.HashMap; +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.admin.data.telemedicine.M_UserTemp; +import com.iemr.admin.data.telemedicine.UserVideoConsultation; +import com.iemr.admin.data.telemedicine.VideoConsultationDomain; +import com.iemr.admin.repo.telemedicine.UserRepo; +import com.iemr.admin.repo.telemedicine.UserVideoConsultationRepo; +import com.iemr.admin.repo.telemedicine.VideoConsultationDomainRepo; +import com.iemr.admin.utils.exception.VideoConsultationException; + +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.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The video consultation service keeps a clinician's account on the external + * conferencing platform in step with the account this system stores. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VideoConsultationServiceImpl Test Suite") +class VideoConsultationServiceImplTest { + + private static final Long USER_ID = 3117L; + private static final Long MAP_ID = 8001L; + private static final Long REMOTE_ID = 990011L; + private static final Integer PROVIDER_ID = 5; + + @Mock + private UserRepo userRepo; + + @Mock + private UserVideoConsultationRepo userVideoConsultationRepo; + + @Mock + private VideoConsultationDomainRepo videoConsultationDomainRepo; + + @Mock + private VideoConsultationAPIInter videoConsultationAPIInter; + + @InjectMocks + private VideoConsultationServiceImpl service; + + private static M_UserTemp clinician() { + M_UserTemp user = new M_UserTemp(); + user.setUserID(USER_ID); + user.setFirstName("Asha"); + user.setLastName("Rao"); + user.setUserName("asha.rao"); + return user; + } + + private static UserVideoConsultation account() { + UserVideoConsultation account = new UserVideoConsultation(); + account.setUserVideoConsultationMapID(MAP_ID); + account.setUserID(USER_ID); + account.setVideoConsultationEmailID("asha.rao@example.org"); + account.setVideoConsultationPassword("secret"); + account.setVideoConsultationDomain("psmri"); + account.setModifiedBy("admin"); + return account; + } + + @Test + @DisplayName("getunmappedUser should answer the clinicians who have no account yet") + void getunmappedUser_shouldAnswerCliniciansWithoutAccount() { + ArrayList free = new ArrayList<>(List.of(clinician())); + when(userRepo.getunmappedVideoConsultationUser(PROVIDER_ID, 7)).thenReturn(free); + + assertSame(free, service.getunmappedUser(PROVIDER_ID, 7)); + } + + @Test + @DisplayName("createUser should open the remote account and record the id it was given") + void createUser_shouldOpenRemoteAccountAndRecordId() throws Exception { + UserVideoConsultation request = account(); + when(userRepo.findByUserID(USER_ID)).thenReturn(clinician()); + when(videoConsultationAPIInter.createUser(any())).thenReturn(REMOTE_ID); + when(userVideoConsultationRepo.save(request)).thenReturn(request); + + assertSame(request, service.createUser(request)); + assertEquals(REMOTE_ID, request.getVideoConsultationID()); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(HashMap.class); + verify(videoConsultationAPIInter).createUser(captor.capture()); + assertEquals("Asha", captor.getValue().get("name")); + assertEquals("Rao", captor.getValue().get("surname")); + assertEquals("asha.rao", captor.getValue().get("member")); + assertEquals("psmri", captor.getValue().get("domain")); + } + + @Test + @DisplayName("createUser should refuse a clinician this system does not know") + void createUser_shouldRefuseUnknownClinician() throws Exception { + when(userRepo.findByUserID(USER_ID)).thenReturn(null); + + VideoConsultationException refusal = assertThrows(VideoConsultationException.class, + () -> service.createUser(account())); + + assertEquals("Invalid User", refusal.getMessage()); + verify(videoConsultationAPIInter, never()).createUser(any()); + } + + @Test + @DisplayName("createUser should give up when the remote platform refuses the account") + void createUser_shouldGiveUpWhenRemoteRefuses() throws Exception { + when(userRepo.findByUserID(USER_ID)).thenReturn(clinician()); + when(videoConsultationAPIInter.createUser(any())) + .thenThrow(new VideoConsultationException("email already registered")); + + assertThrows(VideoConsultationException.class, () -> service.createUser(account())); + verify(userVideoConsultationRepo, never()).save(any()); + } + + @Test + @DisplayName("editUser should record the new sign-in details against the stored account") + void editUser_shouldRecordNewSignInDetails() throws Exception { + UserVideoConsultation stored = account(); + stored.setVideoConsultationID(REMOTE_ID); + stored.setVideoConsultationPassword("old-secret"); + UserVideoConsultation request = account(); + request.setVideoConsultationPassword("new-secret"); + when(userVideoConsultationRepo.findByUserVideoConsultationMapID(MAP_ID)).thenReturn(stored); + when(userVideoConsultationRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.editUser(request)); + assertEquals("new-secret", stored.getVideoConsultationPassword()); + assertNull(stored.getUser(), "the clinician record must not travel back with the account"); + } + + @Test + @DisplayName("editUser should leave the remote platform alone when nothing about the sign-in changed") + void editUser_shouldLeaveRemoteAloneWhenNothingChanged() throws Exception { + UserVideoConsultation stored = account(); + stored.setVideoConsultationID(REMOTE_ID); + when(userVideoConsultationRepo.findByUserVideoConsultationMapID(MAP_ID)).thenReturn(stored); + when(userVideoConsultationRepo.save(stored)).thenReturn(stored); + + service.editUser(account()); + + verify(videoConsultationAPIInter, never()).editUser(any(), anyLong(), anyString()); + } + + @Test + @DisplayName("editUser should open a remote account when the stored one never got an id") + void editUser_shouldOpenRemoteAccountWhenIdMissing() throws Exception { + UserVideoConsultation stored = account(); + when(userVideoConsultationRepo.findByUserVideoConsultationMapID(MAP_ID)).thenReturn(stored); + when(userRepo.findByUserID(USER_ID)).thenReturn(clinician()); + when(videoConsultationAPIInter.createUser(any())).thenReturn(REMOTE_ID); + when(userVideoConsultationRepo.save(stored)).thenReturn(stored); + + assertSame(stored, service.editUser(account())); + assertEquals(REMOTE_ID, stored.getVideoConsultationID()); + } + + @Test + @DisplayName("editUser should refuse an account this system does not know") + void editUser_shouldRefuseUnknownAccount() { + when(userVideoConsultationRepo.findByUserVideoConsultationMapID(MAP_ID)).thenReturn(null); + + VideoConsultationException refusal = assertThrows(VideoConsultationException.class, + () -> service.editUser(account())); + + assertEquals("Invalid MapID", refusal.getMessage()); + } + + @Test + @DisplayName("fetchmappedUser should answer the accounts held under the provider asked for") + void fetchmappedUser_shouldAnswerAccountsOfProvider() { + List held = List.of(account()); + when(userVideoConsultationRepo.fetchmappedUser(PROVIDER_ID)).thenReturn(held); + + assertSame(held, service.fetchmappedUser(PROVIDER_ID)); + } + + @Test + @DisplayName("deleteUser should carry the new status to the remote platform") + void deleteUser_shouldCarryStatusRemotely() throws Exception { + UserVideoConsultation stored = account(); + stored.setVideoConsultationID(REMOTE_ID); + when(userVideoConsultationRepo.findByUserVideoConsultationMapID(MAP_ID)).thenReturn(stored); + when(userVideoConsultationRepo.save(stored)).thenReturn(stored); + when(videoConsultationAPIInter.editUser(any(), anyLong(), anyString())).thenReturn(REMOTE_ID); + + assertSame(stored, service.deleteUser(MAP_ID, Boolean.TRUE, "supervisor")); + assertEquals(Boolean.TRUE, stored.getDeleted()); + assertEquals("supervisor", stored.getModifiedBy()); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(HashMap.class); + verify(videoConsultationAPIInter).editUser(captor.capture(), anyLong(), anyString()); + assertEquals("1", captor.getValue().get("status")); + } + + @Test + @DisplayName("deleteUser should carry the reinstated status when the account is brought back") + void deleteUser_shouldCarryReinstatedStatus() throws Exception { + UserVideoConsultation stored = account(); + stored.setVideoConsultationID(REMOTE_ID); + when(userVideoConsultationRepo.findByUserVideoConsultationMapID(MAP_ID)).thenReturn(stored); + when(userVideoConsultationRepo.save(stored)).thenReturn(stored); + + service.deleteUser(MAP_ID, Boolean.FALSE, "supervisor"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(HashMap.class); + verify(videoConsultationAPIInter).editUser(captor.capture(), anyLong(), anyString()); + assertEquals("0", captor.getValue().get("status")); + } + + @Test + @DisplayName("deleteUser should give up when the account is unknown") + void deleteUser_shouldGiveUpForUnknownAccount() { + when(userVideoConsultationRepo.findByUserVideoConsultationMapID(MAP_ID)).thenReturn(null); + + assertThrows(NullPointerException.class, () -> service.deleteUser(MAP_ID, Boolean.TRUE, "supervisor")); + } + + @Test + @DisplayName("getdomain should answer every conferencing domain on file") + void getdomain_shouldAnswerEveryDomain() { + List domains = List.of(new VideoConsultationDomain()); + when(videoConsultationDomainRepo.findAll()).thenReturn(domains); + + assertEquals(domains, service.getdomain(PROVIDER_ID)); + } +} diff --git a/src/test/java/com/iemr/admin/service/uom/UomServiceImplTest.java b/src/test/java/com/iemr/admin/service/uom/UomServiceImplTest.java new file mode 100644 index 0000000..5770d6e --- /dev/null +++ b/src/test/java/com/iemr/admin/service/uom/UomServiceImplTest.java @@ -0,0 +1,113 @@ +/* +* 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.admin.service.uom; + +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.admin.data.uom.M_Uom; +import com.iemr.admin.repo.uom.UomRepo; + +import static org.junit.jupiter.api.Assertions.assertFalse; +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.anyList; +import static org.mockito.Mockito.when; + +/** The unit of measure service is thin over its repository, but it answers nothing rather than an empty batch. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("UomServiceImpl Test Suite") +class UomServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer RECORD_ID = 11; + + @Mock + private UomRepo uomRepo; + + @InjectMocks + private UomServiceImpl service; + + @Test + @DisplayName("createDrugtypeData should answer the records it stored") + void create_shouldAnswerStoredRecords() { + ArrayList stored = new ArrayList<>(List.of(new M_Uom())); + when(uomRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.createDrugtypeData(new ArrayList<>())); + } + + @Test + @DisplayName("createDrugtypeData should answer nothing when the store took nothing") + void create_shouldAnswerNothingWhenNothingStored() { + when(uomRepo.saveAll(anyList())).thenReturn(new ArrayList()); + + assertNull(service.createDrugtypeData(new ArrayList<>())); + } + + @Test + @DisplayName("the lookups should each reach their own repository query") + void lookups_shouldReachTheirOwnQuery() { + M_Uom record = new M_Uom(); + ArrayList records = new ArrayList<>(List.of(record)); + when(uomRepo.getUom(PSM_ID)).thenReturn(records); + when(uomRepo.geteditedData(RECORD_ID)).thenReturn(record); + when(uomRepo.save(record)).thenReturn(record); + + assertSame(records, service.createDrugtypeData(PSM_ID)); + assertSame(record, service.editDrugtypeData(RECORD_ID)); + assertSame(record, service.saveeditedData(record)); + } + + @Test + @DisplayName("checkUomCode should report a code the provider already uses") + void check_shouldReportUsedCode() { + M_Uom request = new M_Uom(); + request.setuOMCode("C-1"); + request.setProviderServiceMapID(PSM_ID); + when(uomRepo.findByUOMCodeAndProviderServiceMapID("C-1", PSM_ID)).thenReturn(List.of(new M_Uom())); + + assertTrue(service.checkUomCode(request)); + } + + @Test + @DisplayName("checkUomCode should clear a code nobody uses yet") + void check_shouldClearFreeCode() { + M_Uom request = new M_Uom(); + request.setuOMCode("C-2"); + request.setProviderServiceMapID(PSM_ID); + when(uomRepo.findByUOMCodeAndProviderServiceMapID("C-2", PSM_ID)).thenReturn(new ArrayList()); + + assertFalse(service.checkUomCode(request)); + } +} diff --git a/src/test/java/com/iemr/admin/service/uptsu/FacilityServiceImplTest.java b/src/test/java/com/iemr/admin/service/uptsu/FacilityServiceImplTest.java new file mode 100644 index 0000000..e39f211 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/uptsu/FacilityServiceImplTest.java @@ -0,0 +1,290 @@ +/* +* 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.admin.service.uptsu; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Set; + +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +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.admin.data.uptsu.CDSSMapping; +import com.iemr.admin.data.uptsu.M_FacilityMapping; +import com.iemr.admin.data.uptsu.UploadRequest; +import com.iemr.admin.repository.uptsu.CDSSMappingRepo; +import com.iemr.admin.repository.uptsu.FacilityRepository; +import com.iemr.admin.utils.exception.IEMRException; + +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.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.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The UP TSU facility upload turns an operator's spreadsheet into the facility + * mapping rows the state's field staff are attached to, so a column read into + * the wrong field would post a health worker to the wrong facility. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("FacilityServiceImpl Test Suite") +class FacilityServiceImplTest { + + private static final Integer PSM_ID = 4001; + + /** The columns the upload reads as whole numbers rather than text. */ + private static final Set NUMERIC_COLUMNS = Set.of(7, 17, 19, 21, 23, 25, 27); + + /** The columns the upload refuses to accept blank. */ + private static final Set MANDATORY_COLUMNS = Set.of(0, 1, 15, 22, 28, 29, 39); + + @Mock + private FacilityRepository uptsuUploadRepository; + + @Mock + private CDSSMappingRepo cdssMappingRepo; + + @InjectMocks + private FacilityServiceImpl service; + + private static String uploadOf(Workbook workbook) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + workbook.write(out); + workbook.close(); + return "data:application/vnd.ms-excel;base64," + Base64.getEncoder().encodeToString(out.toByteArray()); + } + + /** Builds a one-row upload whose every column carries a usable value. */ + private static Workbook completeUpload() { + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Facilities"); + Row header = sheet.createRow(0); + Row data = sheet.createRow(1); + for (int column = 0; column <= 40; column++) { + header.createCell(column).setCellValue("column " + column); + if (NUMERIC_COLUMNS.contains(column)) { + data.createCell(column).setCellValue(column + 100); + } else { + data.createCell(column).setCellValue("value " + column); + } + } + return workbook; + } + + private static UploadRequest requestFor(Workbook workbook) throws Exception { + UploadRequest request = new UploadRequest(); + request.setCreatedBy("admin"); + request.setProviderServiceMapID(PSM_ID); + request.setFileName("facilities.xlsx"); + request.setFileExtension("xlsx"); + request.setFileContent(uploadOf(workbook)); + return request; + } + + @Test + @DisplayName("saveFacility should turn each spreadsheet row into a facility mapping") + void saveFacility_shouldMapEachRow() throws Exception { + UploadRequest request = requestFor(completeUpload()); + when(uptsuUploadRepository.saveAll(anyList())).thenAnswer(call -> call.getArgument(0)); + + Iterable saved = service.saveFacility(request); + + List rows = new ArrayList<>(); + saved.forEach(rows::add); + assertEquals(1, rows.size()); + M_FacilityMapping mapped = rows.get(0); + assertEquals("value 0", mapped.getEmployeeCode()); + assertEquals("value 1", mapped.getEmployeeName()); + assertEquals(107, mapped.getDesignationId(), "column 7 is read as a whole number"); + assertEquals("value 22", mapped.getBlockName()); + assertEquals("value 39", mapped.getHfrCode()); + assertEquals("admin", mapped.getCreatedBy()); + assertEquals(PSM_ID, mapped.getProviderServiceMapID()); + assertEquals('N', mapped.getProcessed()); + assertEquals(false, mapped.isDeleted()); + } + + @Test + @DisplayName("saveFacility should retire the provider's previous upload before storing the new one") + void saveFacility_shouldRetirePreviousUpload() throws Exception { + UploadRequest request = requestFor(completeUpload()); + when(uptsuUploadRepository.saveAll(anyList())).thenAnswer(call -> call.getArgument(0)); + + service.saveFacility(request); + + verify(uptsuUploadRepository).updatedeleteStatus(org.mockito.ArgumentMatchers.eq(PSM_ID), any(), anyString()); + } + + @Test + @DisplayName("saveFacility should store every row of a multi-row upload") + void saveFacility_shouldStoreEveryRow() throws Exception { + Workbook workbook = completeUpload(); + Sheet sheet = workbook.getSheetAt(0); + Row second = sheet.createRow(2); + for (int column = 0; column <= 40; column++) { + if (NUMERIC_COLUMNS.contains(column)) { + second.createCell(column).setCellValue(column + 200); + } else { + second.createCell(column).setCellValue("second " + column); + } + } + when(uptsuUploadRepository.saveAll(anyList())).thenAnswer(call -> call.getArgument(0)); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + service.saveFacility(requestFor(workbook)); + + verify(uptsuUploadRepository).saveAll(captor.capture()); + assertEquals(2, captor.getValue().size()); + } + + @Test + @DisplayName("saveFacility should leave an optional column unset rather than invent a value") + void saveFacility_shouldLeaveOptionalColumnUnset() throws Exception { + Workbook workbook = completeUpload(); + workbook.getSheetAt(0).getRow(1).getCell(2).setBlank(); + when(uptsuUploadRepository.saveAll(anyList())).thenAnswer(call -> call.getArgument(0)); + + Iterable saved = service.saveFacility(requestFor(workbook)); + + assertNull(saved.iterator().next().getSurveyFacility()); + } + + @Test + @DisplayName("saveFacility should fall back to zero for a numeric column left blank") + void saveFacility_shouldFallBackToZeroForBlankNumericColumn() throws Exception { + Workbook workbook = completeUpload(); + workbook.getSheetAt(0).getRow(1).getCell(7).setBlank(); + when(uptsuUploadRepository.saveAll(anyList())).thenAnswer(call -> call.getArgument(0)); + + Iterable saved = service.saveFacility(requestFor(workbook)); + + assertEquals(0, saved.iterator().next().getDesignationId()); + } + + @Test + @DisplayName("saveFacility should refuse an upload whose mandatory column is blank") + void saveFacility_shouldRefuseBlankMandatoryColumn() throws Exception { + Workbook workbook = completeUpload(); + workbook.getSheetAt(0).getRow(1).getCell(0).setBlank(); + UploadRequest request = requestFor(workbook); + + assertThrows(IEMRException.class, () -> service.saveFacility(request)); + } + + @Test + @DisplayName("saveFacility should read a boolean cell rather than refuse it") + void saveFacility_shouldReadBooleanCell() throws Exception { + Workbook workbook = completeUpload(); + workbook.getSheetAt(0).getRow(1).getCell(2).setCellValue(true); + when(uptsuUploadRepository.saveAll(anyList())).thenAnswer(call -> call.getArgument(0)); + + Iterable saved = service.saveFacility(requestFor(workbook)); + + assertEquals("true", saved.iterator().next().getSurveyFacility()); + } + + @Test + @DisplayName("saveFacility should answer nothing when there is no upload to read") + void saveFacility_shouldAnswerNothingWithoutAnUpload() throws Exception { + assertNull(service.saveFacility(null)); + verify(uptsuUploadRepository, org.mockito.Mockito.never()).saveAll(anyList()); + } + + @Test + @DisplayName("saveFacility should refuse a payload that is not a workbook at all") + void saveFacility_shouldRefuseNonWorkbookPayload() { + UploadRequest request = new UploadRequest(); + request.setCreatedBy("admin"); + request.setProviderServiceMapID(PSM_ID); + request.setFileContent("data:text/plain;base64," + Base64.getEncoder().encodeToString("not a workbook".getBytes())); + + assertThrows(Exception.class, () -> service.saveFacility(request)); + } + + @Test + @DisplayName("saveCdssDetails should retire the provider's previous configuration before storing the new one") + void saveCdssDetails_shouldRetirePreviousConfiguration() { + CDSSMapping previous = new CDSSMapping(); + previous.setPsmId(PSM_ID); + CDSSMapping request = new CDSSMapping(); + request.setPsmId(PSM_ID); + request.setIsCdss(Boolean.TRUE); + when(cdssMappingRepo.findByPsmIdAndDeleted(PSM_ID, false)).thenReturn(previous); + when(cdssMappingRepo.save(request)).thenReturn(request); + + assertEquals(request, service.saveCdssDetails(request)); + assertTrue(previous.getDeleted(), "the configuration it replaces must be retired"); + } + + @Test + @DisplayName("saveCdssDetails should store the first configuration a provider has") + void saveCdssDetails_shouldStoreFirstConfiguration() { + CDSSMapping request = new CDSSMapping(); + request.setPsmId(PSM_ID); + when(cdssMappingRepo.findByPsmIdAndDeleted(PSM_ID, false)).thenReturn(null); + when(cdssMappingRepo.save(request)).thenReturn(request); + + assertEquals(request, service.saveCdssDetails(request)); + } + + @Test + @DisplayName("getCdssData should publish the stored configuration as JSON") + void getCdssData_shouldPublishStoredConfiguration() throws Exception { + CDSSMapping stored = new CDSSMapping(); + stored.setPsmId(PSM_ID); + stored.setIsCdss(Boolean.TRUE); + when(cdssMappingRepo.findByPsmIdAndDeleted(PSM_ID, false)).thenReturn(stored); + + String published = service.getCdssData(PSM_ID); + + assertTrue(published.contains("\"psmId\":4001"), published); + assertTrue(published.contains("\"isCdss\":true"), published); + } + + @Test + @DisplayName("getCdssData should publish a null rather than fail for a provider with no configuration") + void getCdssData_shouldPublishNullForUnconfiguredProvider() throws Exception { + when(cdssMappingRepo.findByPsmIdAndDeleted(anyInt(), any())).thenReturn(null); + + assertEquals("null", service.getCdssData(PSM_ID)); + } +} diff --git a/src/test/java/com/iemr/admin/service/user/EncryptUserPasswordTest.java b/src/test/java/com/iemr/admin/service/user/EncryptUserPasswordTest.java new file mode 100644 index 0000000..98deab2 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/user/EncryptUserPasswordTest.java @@ -0,0 +1,171 @@ +/* +* 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.admin.service.user; + +import org.json.JSONObject; +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.admin.data.employeemaster.M_User1; +import com.iemr.admin.data.user.M_User; +import com.iemr.admin.service.provideronboard.EncryptUserPassword123; +import com.iemr.admin.utils.http.HttpUtils; +import com.iemr.admin.utils.response.OutputResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Passwords are never stored by this service itself: they are handed to the + * common service, which is stood in for here. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("Password encryption Test Suite") +class EncryptUserPasswordTest { + + private static final String BASE_URL = "https://common.example.org"; + + @Mock + private HttpUtils httpUtils; + + private HttpUtils originalEmployeeUtils; + private HttpUtils originalOnboardUtils; + + private EncryptUserPassword service; + private EncryptUserPassword123 onboardService; + + @BeforeEach + @DisplayName("Stand in for the common service on both callers") + void setUp() { + originalEmployeeUtils = (HttpUtils) ReflectionTestUtils.getField(EncryptUserPassword.class, "utils"); + originalOnboardUtils = (HttpUtils) ReflectionTestUtils.getField(EncryptUserPassword123.class, "utils"); + ReflectionTestUtils.setField(EncryptUserPassword.class, "utils", httpUtils); + ReflectionTestUtils.setField(EncryptUserPassword123.class, "utils", httpUtils); + + service = new EncryptUserPassword(); + ReflectionTestUtils.setField(service, "commonBaseURL", BASE_URL); + service.init(); + + onboardService = new EncryptUserPassword123(); + ReflectionTestUtils.setField(onboardService, "commonBaseURL", BASE_URL); + onboardService.init(); + } + + @AfterEach + @DisplayName("Put the real common service client back so no other suite sees the stand-in") + void tearDown() { + ReflectionTestUtils.setField(EncryptUserPassword.class, "utils", originalEmployeeUtils); + ReflectionTestUtils.setField(EncryptUserPassword123.class, "utils", originalOnboardUtils); + } + + private static M_User1 employee() { + M_User1 employee = new M_User1(); + employee.setUserName("asha.rao"); + employee.setPassword("secret"); + return employee; + } + + private static M_User administrator() { + M_User administrator = new M_User(); + administrator.setUserName("admin.rao"); + administrator.setPassword("other-secret"); + return administrator; + } + + @Test + @DisplayName("init should address the common service under the configured base address") + void init_shouldAddressConfiguredCommonService() { + assertTrue(((String) ReflectionTestUtils.getField(service, "encryptPasswordURL")).startsWith(BASE_URL), + "the encryption address must sit under the configured common service"); + } + + @Test + @DisplayName("encryptUserCredentials should hand the employee's credentials to the common service") + void encryptUserCredentials_shouldHandEmployeeCredentialsOver() { + when(httpUtils.post(anyString(), anyString())).thenReturn("{\"statusCode\":200,\"status\":\"Success\"}"); + ArgumentCaptor body = ArgumentCaptor.forClass(String.class); + + OutputResponse response = service.encryptUserCredentials(employee()); + + assertNotNull(response); + verify(httpUtils).post(anyString(), body.capture()); + JSONObject sent = new JSONObject(body.getValue()); + assertEquals("asha.rao", sent.getString("userName")); + assertEquals("secret", sent.getString("password")); + assertTrue(sent.getBoolean("isAdmin"), "an employee account created here is created as an administrator"); + } + + @Test + @DisplayName("encryptUserCredentials should carry back what the common service answered") + void encryptUserCredentials_shouldCarryBackTheAnswer() { + when(httpUtils.post(anyString(), anyString())) + .thenReturn("{\"statusCode\":5000,\"errorMessage\":\"user not found\"}"); + + assertEquals(5000, service.encryptUserCredentials(employee()).getStatusCode()); + } + + @Test + @DisplayName("encryptUserCredentials should give up when the common service answers something unreadable") + void encryptUserCredentials_shouldGiveUpOnUnreadableAnswer() { + when(httpUtils.post(anyString(), anyString())).thenReturn("502 Bad Gateway"); + + assertThrows(RuntimeException.class, () -> service.encryptUserCredentials(employee())); + } + + @Test + @DisplayName("the onboarding caller should hand the administrator's credentials over without the admin flag") + void onboardingCaller_shouldHandCredentialsOverWithoutAdminFlag() { + when(httpUtils.post(anyString(), anyString())).thenReturn("{\"statusCode\":200,\"status\":\"Success\"}"); + ArgumentCaptor body = ArgumentCaptor.forClass(String.class); + + assertNotNull(onboardService.encryptUserCredentials(administrator())); + + verify(httpUtils).post(anyString(), body.capture()); + JSONObject sent = new JSONObject(body.getValue()); + assertEquals("admin.rao", sent.getString("userName")); + assertEquals("other-secret", sent.getString("password")); + assertTrue(!sent.has("isAdmin"), "the onboarding caller does not claim the administrator flag"); + } + + @Test + @DisplayName("the onboarding caller should give up when the common service answers something unreadable") + void onboardingCaller_shouldGiveUpOnUnreadableAnswer() { + when(httpUtils.post(anyString(), anyString())).thenReturn("gateway timeout"); + + assertThrows(RuntimeException.class, () -> onboardService.encryptUserCredentials(administrator())); + } +} diff --git a/src/test/java/com/iemr/admin/service/user/IemrUserServiceImplTest.java b/src/test/java/com/iemr/admin/service/user/IemrUserServiceImplTest.java new file mode 100644 index 0000000..860374b --- /dev/null +++ b/src/test/java/com/iemr/admin/service/user/IemrUserServiceImplTest.java @@ -0,0 +1,283 @@ +/* +* 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.admin.service.user; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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.admin.data.user.M_User; +import com.iemr.admin.data.user.M_UserServiceRoleMapping; +import com.iemr.admin.repository.user.IemrUserRepositoryImplCustom; +import com.iemr.admin.repository.user.M_UserMappingRepo; +import com.iemr.admin.service.provideronboard.EncryptUserPassword123; + +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.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The user service turns the loosely typed sign-up form the onboarding screen + * sends into a stored administrator account, then has its password encrypted. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("IemrUserServiceImpl Test Suite") +class IemrUserServiceImplTest { + + private static final int USER_ID = 3117; + + @Mock + private EncryptUserPassword123 encryptUserPassword; + + @Mock + private IemrUserRepositoryImplCustom iemrUserRepositoryImplCustom; + + @Mock + private M_UserMappingRepo m_UserMappingRepo; + + @InjectMocks + private IemrUserServiceImpl service; + + private static Map signUpForm() { + Map form = new HashMap<>(); + form.put("titleID", 1); + form.put("firstName", "Asha"); + form.put("middleName", "K"); + form.put("lastName", "Rao"); + form.put("genderID", 2); + form.put("maritalStatusID", "1"); + form.put("aadhaarNo", "1234"); + form.put("aadharNo", "123412341234"); + form.put("panNo", "ABCDE1234F"); + form.put("dob", "1990-05-17T00:00:00+05:30"); + form.put("doj", "2020-01-06T00:00:00+05:30"); + form.put("qualificationID", "4"); + form.put("userName", "asha.rao"); + form.put("password", "secret"); + form.put("emailID", "asha.rao@example.org"); + form.put("emrContactPersion", "Ravi Rao"); + form.put("emrConctactNo", "9000000001"); + form.put("isSupervisor", Boolean.TRUE); + form.put("deleted", Boolean.FALSE); + form.put("statusID", 1); + return form; + } + + private static ArrayList> formList(Map form) { + ArrayList> list = new ArrayList<>(); + list.add(form); + return list; + } + + private static M_User storedUser() { + M_User user = new M_User(); + user.setUserID(USER_ID); + user.setUserName("asha.rao"); + return user; + } + + @SuppressWarnings("unchecked") + private M_User captureSavedUser() { + ArgumentCaptor> captor = ArgumentCaptor.forClass(Set.class); + verify(iemrUserRepositoryImplCustom).saveAll(captor.capture()); + return captor.getValue().iterator().next(); + } + + @Test + @DisplayName("createUser should record every detail the sign-up form carried") + void createUser_shouldRecordEveryFormDetail() { + when(iemrUserRepositoryImplCustom.saveAll(anySet())).thenReturn(List.of(storedUser())); + + assertEquals(USER_ID, service.createUser(formList(signUpForm()), "admin")); + + M_User saved = captureSavedUser(); + assertEquals(1, saved.getTitleID()); + assertEquals("Asha", saved.getFirstName()); + assertEquals("K", saved.getMiddleName()); + assertEquals("Rao", saved.getLastName()); + assertEquals(2, saved.getGenderID()); + assertEquals(1, saved.getMaritalStatusID()); + assertEquals("123412341234", saved.getAadhaarNo(), + "the Aadhaar number is read from the differently spelled key the form sends"); + assertEquals("ABCDE1234F", saved.getPAN()); + assertEquals(Timestamp.valueOf("1990-05-17 00:00:00"), saved.getDOB()); + assertEquals(Timestamp.valueOf("2020-01-06 00:00:00"), saved.getDOJ()); + assertEquals(4, saved.getQualificationID()); + assertEquals("asha.rao", saved.getUserName()); + assertEquals("secret", saved.getPassword()); + assertEquals("asha.rao@example.org", saved.getEmailID()); + assertEquals("Ravi Rao", saved.getEmergencyContactPerson()); + assertEquals("9000000001", saved.getEmergencyContactNo()); + assertEquals(Boolean.TRUE, saved.isIsSupervisor()); + assertEquals(false, saved.isDeleted()); + assertEquals(1, saved.getStatusID()); + assertEquals("admin", saved.getCreatedBy()); + } + + @Test + @DisplayName("createUser should have the stored account's password encrypted") + void createUser_shouldHavePasswordEncrypted() { + M_User stored = storedUser(); + when(iemrUserRepositoryImplCustom.saveAll(anySet())).thenReturn(List.of(stored)); + + service.createUser(formList(signUpForm()), "admin"); + + verify(encryptUserPassword).encryptUserCredentials(stored); + } + + @Test + @DisplayName("createUser should record an anonymous author when the caller does not name one") + void createUser_shouldRecordAnonymousAuthor() { + when(iemrUserRepositoryImplCustom.saveAll(anySet())).thenReturn(List.of(storedUser())); + + service.createUser(formList(signUpForm()), null); + + assertEquals("", captureSavedUser().getCreatedBy()); + } + + @Test + @DisplayName("createUser should leave out the details the sign-up form did not carry") + void createUser_shouldLeaveOutMissingDetails() { + Map sparse = new HashMap<>(); + sparse.put("userName", "asha.rao"); + when(iemrUserRepositoryImplCustom.saveAll(anySet())).thenReturn(List.of(storedUser())); + + service.createUser(formList(sparse), "admin"); + + M_User saved = captureSavedUser(); + assertEquals("asha.rao", saved.getUserName()); + assertNull(saved.getFirstName()); + assertNull(saved.getDOB()); + assertNull(saved.getQualificationID()); + } + + @Test + @DisplayName("createUser should ignore a date the form sent as a bare day with no time") + void createUser_shouldIgnoreBareDayDates() { + Map form = signUpForm(); + form.put("dob", "1990-05-17"); + form.put("doj", "2020-01-06"); + form.put("maritalStatusID", ""); + form.put("qualificationID", ""); + when(iemrUserRepositoryImplCustom.saveAll(anySet())).thenReturn(List.of(storedUser())); + + service.createUser(formList(form), "admin"); + + M_User saved = captureSavedUser(); + assertNull(saved.getDOB(), "a date with no time is not a timestamp this service can store"); + assertNull(saved.getDOJ()); + assertNull(saved.getMaritalStatusID()); + assertNull(saved.getQualificationID()); + } + + @Test + @DisplayName("createUser should answer no account when nothing was stored") + void createUser_shouldAnswerNoAccountWhenNothingStored() { + when(iemrUserRepositoryImplCustom.saveAll(anySet())).thenReturn(new ArrayList()); + + assertEquals(0, service.createUser(formList(signUpForm()), "admin")); + verify(encryptUserPassword, never()).encryptUserCredentials(any()); + } + + @Test + @DisplayName("createUser should give up when the account cannot be stored") + void createUser_shouldGiveUpWhenStorageFails() { + when(iemrUserRepositoryImplCustom.saveAll(anySet())) + .thenThrow(new RuntimeException("duplicate user name")); + + assertThrows(RuntimeException.class, () -> service.createUser(formList(signUpForm()), "admin")); + } + + @Test + @DisplayName("createUser should give up when the form carries a date it cannot read") + void createUser_shouldGiveUpOnUnreadableDate() { + Map form = signUpForm(); + form.put("dob", "the seventeenth of May"); + + assertThrows(RuntimeException.class, () -> service.createUser(formList(form), "admin")); + } + + @Test + @DisplayName("createUserServiceRoleMapping should give the new administrator a role on every service line named") + void createRoleMapping_shouldGiveRoleOnEveryServiceLine() { + when(m_UserMappingRepo.saveAll(anySet())) + .thenReturn(new ArrayList<>(List.of(new M_UserServiceRoleMapping()))); + + assertEquals(1, service.createUserServiceRoleMapping(List.of(4001, 4002), USER_ID, "admin")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Set.class); + verify(m_UserMappingRepo).saveAll(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertTrue(captor.getValue().stream().allMatch(one -> one.getRoleID() == 11), + "an onboarded administrator always takes the administrator role"); + assertTrue(captor.getValue().stream().allMatch(one -> one.getUserID() == USER_ID)); + } + + @Test + @DisplayName("createUserServiceRoleMapping should report nothing recorded when no service line was named") + void createRoleMapping_shouldReportNothingRecordedWithoutServiceLines() { + when(m_UserMappingRepo.saveAll(anySet())).thenReturn(new ArrayList()); + + assertEquals(0, service.createUserServiceRoleMapping(new ArrayList<>(), USER_ID, "admin")); + } + + @Test + @DisplayName("createUserServiceRoleMapping should give up when the roles cannot be recorded") + void createRoleMapping_shouldGiveUpWhenStorageFails() { + when(m_UserMappingRepo.saveAll(anySet())).thenThrow(new RuntimeException("row is locked")); + + assertThrows(RuntimeException.class, + () -> service.createUserServiceRoleMapping(List.of(4001), USER_ID, "admin")); + } + + @Test + @DisplayName("the collaborators should be replaceable so the service can be wired by hand") + void collaborators_shouldBeReplaceable() { + IemrUserServiceImpl standalone = new IemrUserServiceImpl(); + standalone.setIemrUserRepositoryImplCustom(iemrUserRepositoryImplCustom); + standalone.setM_UserMappingRepo(m_UserMappingRepo); + when(m_UserMappingRepo.saveAll(anySet())) + .thenReturn(new ArrayList<>(List.of(new M_UserServiceRoleMapping()))); + + assertEquals(1, standalone.createUserServiceRoleMapping(List.of(4001), USER_ID, "admin")); + } +} diff --git a/src/test/java/com/iemr/admin/service/userParkingPlaceMap/UserParkingPlaceMapServiceImplTest.java b/src/test/java/com/iemr/admin/service/userParkingPlaceMap/UserParkingPlaceMapServiceImplTest.java new file mode 100644 index 0000000..8b98a48 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/userParkingPlaceMap/UserParkingPlaceMapServiceImplTest.java @@ -0,0 +1,233 @@ +/* +* 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.admin.service.userParkingPlaceMap; + +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.admin.data.userParkingPlaceMap.M_UserParkingPlaceMap; +import com.iemr.admin.data.userParkingPlaceMap.M_UserVanMapping; +import com.iemr.admin.repo.employeemaster.EmployeeMasterRepo; +import com.iemr.admin.repository.userParkingPlaceMap.UserParkingPlaceMapRepository; +import com.iemr.admin.repository.userParkingPlaceMap.UserVanMappingRepository; + +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; +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.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The user parking place service keeps a field user's posting and the vans that + * posting covers in step, replacing the van list wholesale on every edit. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("UserParkingPlaceMapServiceImpl Test Suite") +class UserParkingPlaceMapServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer MAP_ID = 9001; + private static final Integer USER_ID = 3117; + + @Mock + private UserParkingPlaceMapRepository userParkingPlaceMapRepository; + + @Mock + private UserVanMappingRepository userVanMappingRepository; + + @Mock + private EmployeeMasterRepo employeeMasterRepo; + + @InjectMocks + private UserParkingPlaceMapServiceImpl service; + + private static M_UserParkingPlaceMap posting(Integer id) { + return new M_UserParkingPlaceMap(id, USER_ID, "Asha", "Rao", "asha.rao", 7, 301, 31, "Hosur parking", + PSM_ID, Boolean.FALSE, Boolean.FALSE); + } + + private static M_UserVanMapping vanMapping(Integer id) { + M_UserVanMapping mapping = new M_UserVanMapping(); + mapping.setUserVanMapID(id); + mapping.setVanID(71); + return mapping; + } + + @Test + @DisplayName("saveUserParkingPlaceDetails should attach the vans to each posting it stored") + void save_shouldAttachVansToStoredPostings() { + M_UserParkingPlaceMap stored = posting(MAP_ID); + stored.setCreatedBy("admin"); + stored.setUservanmapping(new ArrayList<>(List.of(vanMapping(null)))); + when(userParkingPlaceMapRepository.saveAll(anyList())) + .thenReturn(new ArrayList<>(List.of(stored))); + + service.saveUserParkingPlaceDetails(new ArrayList<>(List.of(stored))); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(userVanMappingRepository).saveAll(captor.capture()); + assertEquals(1, captor.getValue().size()); + assertEquals(MAP_ID, captor.getValue().get(0).getUserParkingPlaceMapID()); + assertEquals("admin", captor.getValue().get(0).getCreatedBy()); + } + + @Test + @DisplayName("getUserParkingPlaceMappings should pass every filter the caller supplies through") + void getMappings_shouldPassFiltersThrough() { + when(userParkingPlaceMapRepository.getUserParkingPlaceMappings(77, "29", "301", "31", "7")) + .thenReturn(List.of(new Object[] { MAP_ID, USER_ID, "Asha", "Rao", "asha.rao", 7, + "11", "Counsellor", (short) 2, "Female", 29, "Karnataka", 301, "Bengaluru Urban", + 31, "Hosur parking", "9000000001", PSM_ID, Boolean.FALSE })); + + assertEquals(1, service.getUserParkingPlaceMappings(77, 29, 301, 31, 7).size()); + } + + @Test + @DisplayName("getUserParkingPlaceMappings should match everything for a filter the caller leaves blank") + void getMappings_shouldWildcardBlankFilters() { + when(userParkingPlaceMapRepository.getUserParkingPlaceMappings(77, "%%", "%%", "%%", "%%")) + .thenReturn(new ArrayList<>()); + + service.getUserParkingPlaceMappings(77, null, null, null, null); + + verify(userParkingPlaceMapRepository).getUserParkingPlaceMappings(77, "%%", "%%", "%%", "%%"); + } + + @Test + @DisplayName("getUserParkingPlaceMappings1 should rebuild one posting per row the query answers") + void getMappings1_shouldRebuildEachRow() { + when(userParkingPlaceMapRepository.getUserParkingPlaceMappings1(PSM_ID, 31, 7)) + .thenReturn(List.of(new Object[] { MAP_ID, USER_ID, "Asha", "Rao", "asha.rao", 7, + 301, 31, "Hosur parking", PSM_ID, Boolean.FALSE, Boolean.FALSE, "ASHA" })); + + assertEquals(1, service.getUserParkingPlaceMappings1(PSM_ID, 301, 31, 7).size()); + } + + @Test + @DisplayName("the record lookups should each reach their own repository query") + void recordLookups_shouldReachTheirOwnQuery() { + M_UserParkingPlaceMap stored = posting(MAP_ID); + ArrayList postings = new ArrayList<>(List.of(stored)); + when(userParkingPlaceMapRepository.getUserParkingPlaceMapByID(MAP_ID)).thenReturn(stored); + when(userParkingPlaceMapRepository.findByUserParkingPlaceMapID(MAP_ID)).thenReturn(stored); + when(userParkingPlaceMapRepository.save(stored)).thenReturn(stored); + when(userParkingPlaceMapRepository.saveAll(anyList())).thenReturn(postings); + when(userParkingPlaceMapRepository.updateUserParkingPlaceMapStatus(MAP_ID, Boolean.FALSE, null)) + .thenReturn(1); + + assertSame(stored, service.getUserParkingPlaceMapByID(MAP_ID)); + assertSame(stored, service.getUserParkingPlaceDetails(MAP_ID)); + assertSame(stored, service.saveediteddata(stored)); + assertSame(postings, service.saveUserParkingPlaceDetails1(new ArrayList<>())); + assertEquals(1, service.updateUserParkingPlaceMapStatus(stored)); + } + + @Test + @DisplayName("saveediteddata should replace the van list rather than add to it") + void saveEdited_shouldReplaceVanList() { + M_UserParkingPlaceMap stored = posting(MAP_ID); + stored.setModifiedBy("admin"); + M_UserVanMapping fresh = vanMapping(null); + when(userParkingPlaceMapRepository.save(stored)).thenReturn(stored); + when(userVanMappingRepository.saveAll(anyList())) + .thenAnswer(call -> new ArrayList<>((List) call.getArgument(0))); + + M_UserParkingPlaceMap saved = service.saveediteddata(stored, List.of(fresh)); + + verify(userVanMappingRepository).deactivatebyuserparkingplaceid(MAP_ID, "admin"); + assertEquals(1, saved.getUservanmapping().size()); + assertEquals(MAP_ID, saved.getUservanmapping().get(0).getUserParkingPlaceMapID()); + assertNull(saved.getUservanmapping().get(0).getUserParkingPlaceMap(), + "the van mapping must not carry the posting back with it"); + } + + @Test + @DisplayName("getunmappedUser should exclude the users already posted when there are any") + void getunmappedUser_shouldExcludePostedUsers() { + when(userParkingPlaceMapRepository.getmappedids(PSM_ID, 7)).thenReturn(List.of(USER_ID)); + when(employeeMasterRepo.getAllEmpByProviderServiceMapIDAndDesignationNotInUserID(PSM_ID, 7, + List.of(USER_ID))).thenReturn(List.of(new Object[] { 3118, "Ravi Kumar" })); + + assertEquals(1, service.getunmappedUser(PSM_ID, 7).size()); + verify(employeeMasterRepo, never()).getAllEmpByProviderServiceMapIDAndDesignation(anyInt(), anyInt()); + } + + @Test + @DisplayName("getunmappedUser should answer every user when none is posted yet") + void getunmappedUser_shouldAnswerEveryUserWhenNonePosted() { + when(userParkingPlaceMapRepository.getmappedids(PSM_ID, 7)).thenReturn(new ArrayList<>()); + when(employeeMasterRepo.getAllEmpByProviderServiceMapIDAndDesignation(PSM_ID, 7)) + .thenReturn(List.of(new Object[] { 3118, "Ravi Kumar" })); + + assertEquals(1, service.getunmappedUser(PSM_ID, 7).size()); + } + + @Test + @DisplayName("getuserexist should report whether the user already holds a live posting") + void getuserexist_shouldReportWhetherUserIsPosted() { + when(userParkingPlaceMapRepository.findByProviderServiceMapIDAndUserIDAndDeleted(PSM_ID, USER_ID, false)) + .thenReturn(List.of(posting(MAP_ID))); + assertTrue(service.getuserexist(PSM_ID, USER_ID)); + + when(userParkingPlaceMapRepository.findByProviderServiceMapIDAndUserIDAndDeleted(PSM_ID, USER_ID, false)) + .thenReturn(new ArrayList<>()); + assertFalse(service.getuserexist(PSM_ID, USER_ID)); + } + + @Test + @DisplayName("getuservanmapping should answer the vans the posting covers") + void getuservanmapping_shouldAnswerCoveredVans() { + List stored = List.of(vanMapping(7001)); + when(userVanMappingRepository.findByUserParkingPlaceMapIDAndDeleted(MAP_ID)).thenReturn(stored); + + assertSame(stored, service.getuservanmapping(MAP_ID)); + } + + @Test + @DisplayName("deleteuservanmapping should release the van mapping the caller names") + void deleteuservanmapping_shouldReleaseNamedMapping() { + M_UserVanMapping request = vanMapping(7001); + request.setModifiedBy("admin"); + + service.deleteuservanmapping(request); + + verify(userVanMappingRepository).deleteUservanMap(7001, "admin"); + } +} diff --git a/src/test/java/com/iemr/admin/service/vanMaster/VanMasterServiceImplTest.java b/src/test/java/com/iemr/admin/service/vanMaster/VanMasterServiceImplTest.java new file mode 100644 index 0000000..525490d --- /dev/null +++ b/src/test/java/com/iemr/admin/service/vanMaster/VanMasterServiceImplTest.java @@ -0,0 +1,215 @@ +/* +* 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.admin.service.vanMaster; + +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.admin.data.parkingPlace.M_Parkingplace; +import com.iemr.admin.data.vanMaster.M_Van; +import com.iemr.admin.data.vanType.M_VanType; +import com.iemr.admin.repository.parkingPlace.ParkingPlaceRepository; +import com.iemr.admin.repository.vanMaster.VanMasterRepository; +import com.iemr.admin.repository.vanType.VanTypeRepository; + +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.anyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The van service keeps the mobile unit fleet: which vans exist, what type each + * is, and which parking place each is stationed at. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VanMasterServiceImpl Test Suite") +class VanMasterServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer VAN_ID = 71; + private static final Integer PARKING_PLACE_ID = 31; + + @Mock + private VanMasterRepository vanMasterRepository; + + @Mock + private VanTypeRepository vanTypeRepository; + + @Mock + private ParkingPlaceRepository parkingPlaceRepository; + + @InjectMocks + private VanMasterServiceImpl service; + + private static Object[] fleetRow() { + return new Object[] { VAN_ID, "Mobile unit 7", "KA-01-AB-1234", 2, "Diagnostic van", Boolean.FALSE, PSM_ID, + 1, "India", 29, "Karnataka", PARKING_PLACE_ID, "Hosur parking", 3011, "psmri", "990011", + "van7@example.org", false }; + } + + @Test + @DisplayName("getAvailableVans should rebuild one van per row the query answers") + void getAvailableVans_shouldRebuildEachRow() { + when(vanMasterRepository.getAvailableVans("31", "2", PSM_ID)).thenReturn(List.of(fleetRow())); + + ArrayList vans = service.getAvailableVans(PARKING_PLACE_ID, 2, PSM_ID); + + assertEquals(1, vans.size()); + assertEquals("Mobile unit 7", vans.get(0).getVanName()); + assertEquals("KA-01-AB-1234", vans.get(0).getVehicalNo()); + assertEquals("Hosur parking", vans.get(0).getParkingPlaceName()); + } + + @Test + @DisplayName("getAvailableVans should match any parking place or type the caller leaves out") + void getAvailableVans_shouldWildcardOmittedFilters() { + when(vanMasterRepository.getAvailableVans("%%", "%%", PSM_ID)).thenReturn(new ArrayList<>()); + + assertTrue(service.getAvailableVans(null, null, PSM_ID).isEmpty()); + verify(vanMasterRepository).getAvailableVans("%%", "%%", PSM_ID); + } + + @Test + @DisplayName("saveVanDetails and saveVanTypeDetails should answer what the repository stored") + void saveOperations_shouldAnswerStoredRecords() { + ArrayList vans = new ArrayList<>(List.of(new M_Van())); + ArrayList types = new ArrayList<>(List.of(new M_VanType())); + when(vanMasterRepository.saveAll(anyList())).thenReturn(vans); + when(vanTypeRepository.saveAll(anyList())).thenReturn(types); + + assertSame(vans, service.saveVanDetails(new ArrayList<>())); + assertSame(types, service.saveVanTypeDetails(new ArrayList<>())); + } + + @Test + @DisplayName("updateVanStatus should report how many rows the retirement touched") + void updateVanStatus_shouldReportRowsTouched() { + M_Van request = new M_Van(); + request.setVanID(VAN_ID); + request.setDeleted(Boolean.TRUE); + request.setModifiedBy("admin"); + when(vanMasterRepository.updateVanStatus(VAN_ID, Boolean.TRUE, "admin")).thenReturn(1); + + assertEquals(1, service.updateVanStatus(request)); + } + + @Test + @DisplayName("updateVanStatus should report nothing touched when the van is unknown") + void updateVanStatus_shouldReportNothingTouchedForUnknownVan() { + M_Van request = new M_Van(); + request.setVanID(-1); + when(vanMasterRepository.updateVanStatus(-1, null, null)).thenReturn(0); + + assertEquals(0, service.updateVanStatus(request)); + } + + @Test + @DisplayName("updateVanTypeStatus should report how many rows the retirement touched") + void updateVanTypeStatus_shouldReportRowsTouched() { + M_VanType request = new M_VanType(2, "Diagnostic van", "Carries lab kit", Boolean.TRUE); + request.setModifiedBy("admin"); + when(vanTypeRepository.updateVanTypeStatus(2, Boolean.TRUE, "admin")).thenReturn(1); + + assertEquals(1, service.updateVanTypeStatus(request)); + } + + @Test + @DisplayName("getVanTypes should rebuild one van type per row the query answers") + void getVanTypes_shouldRebuildEachRow() { + when(vanTypeRepository.getVanTypes()) + .thenReturn(List.of(new Object[] { 2, "Diagnostic van", "Carries lab kit", Boolean.FALSE })); + + ArrayList types = service.getVanTypes(); + + assertEquals(1, types.size()); + assertEquals("Diagnostic van", types.get(0).getVanType()); + assertEquals("Carries lab kit", types.get(0).getVanTypeDesc()); + } + + @Test + @DisplayName("getVanTypes should answer nothing when no van type is on file") + void getVanTypes_shouldAnswerNothingWhenNoneOnFile() { + when(vanTypeRepository.getVanTypes()).thenReturn(new ArrayList<>()); + + assertTrue(service.getVanTypes().isEmpty()); + } + + @Test + @DisplayName("getVanByID, updateVanData and getVanMaster should each reach their own query") + void singleRecordOperations_shouldReachTheirOwnQuery() { + M_Van stored = new M_Van(); + List stationed = List.of(stored); + when(vanMasterRepository.getVanById(VAN_ID)).thenReturn(stored); + when(vanMasterRepository.save(stored)).thenReturn(stored); + when(vanMasterRepository.findByProviderServiceMapIDAndParkingPlaceID(PSM_ID, PARKING_PLACE_ID)) + .thenReturn(stationed); + + assertSame(stored, service.getVanByID(VAN_ID)); + assertSame(stored, service.updateVanData(stored)); + assertSame(stationed, service.getVanMaster(PSM_ID, PARKING_PLACE_ID)); + } + + @Test + @DisplayName("getVanFromFacilityID should answer the vans stationed at the store's parking place") + void getVanFromFacilityID_shouldAnswerVansAtStoresParkingPlace() throws Exception { + M_Parkingplace place = new M_Parkingplace(); + place.setParkingPlaceID(PARKING_PLACE_ID); + place.setProviderServiceMapID(PSM_ID); + List stationed = List.of(new M_Van()); + when(parkingPlaceRepository.findFirstByFacilityID(9001)).thenReturn(place); + when(vanMasterRepository.findByProviderServiceMapIDAndParkingPlaceID(PSM_ID, PARKING_PLACE_ID)) + .thenReturn(stationed); + + assertSame(stationed, service.getVanFromFacilityID(9001)); + } + + @Test + @DisplayName("getVanFromFacilityID should refuse a store that has no parking place of its own") + void getVanFromFacilityID_shouldRefuseStoreWithoutParkingPlace() { + when(parkingPlaceRepository.findFirstByFacilityID(9001)).thenReturn(null); + + Exception refusal = assertThrows(Exception.class, () -> service.getVanFromFacilityID(9001)); + + assertEquals("Main Store doesnt have any Parking place mapped", refusal.getMessage()); + } + + @Test + @DisplayName("getVanFromFacilityID should refuse a parking place record that names no parking place") + void getVanFromFacilityID_shouldRefuseNamelessParkingPlace() { + when(parkingPlaceRepository.findFirstByFacilityID(9001)).thenReturn(new M_Parkingplace()); + + assertThrows(Exception.class, () -> service.getVanFromFacilityID(9001)); + } +} diff --git a/src/test/java/com/iemr/admin/service/vanServicePointMapping/VanServicePointMappingServiceImplTest.java b/src/test/java/com/iemr/admin/service/vanServicePointMapping/VanServicePointMappingServiceImplTest.java new file mode 100644 index 0000000..ea24d93 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/vanServicePointMapping/VanServicePointMappingServiceImplTest.java @@ -0,0 +1,170 @@ +/* +* 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.admin.service.vanServicePointMapping; + +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.admin.data.vanServicePointMapping.M_VanServicePointMap; +import com.iemr.admin.repository.vanServicePointMapping.VanServicePointMappingRepository; + +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.anyList; +import static org.mockito.Mockito.when; + +/** + * The van service point service records which service points a van visits and + * in which session of the day. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VanServicePointMappingServiceImpl Test Suite") +class VanServicePointMappingServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer VAN_ID = 71; + private static final Integer PARKING_PLACE_ID = 31; + private static final Integer MAP_ID = 8801; + + @Mock + private VanServicePointMappingRepository vanServicePointMappingRepository; + + @InjectMocks + private VanServicePointMappingServiceImpl service; + + private static Object[] mappingRow() { + return new Object[] { MAP_ID, VAN_ID, (short) 1, 88, "Attibele PHC", PSM_ID, Boolean.FALSE }; + } + + private static Object[] detailedMappingRow() { + return new Object[] { MAP_ID, VAN_ID, (short) 1, 88, "Attibele PHC", PSM_ID, Boolean.FALSE, 301, + "Bengaluru Urban", 3011, "Anekal" }; + } + + @Test + @DisplayName("getAvailableVanServicePointMappings should rebuild one visit per row the query answers") + void getAvailable_shouldRebuildEachRow() { + when(vanServicePointMappingRepository.getAvailableVanServicePointMappings(PARKING_PLACE_ID, VAN_ID, PSM_ID)) + .thenReturn(List.of(mappingRow())); + + ArrayList visits = service.getAvailableVanServicePointMappings(PARKING_PLACE_ID, + VAN_ID, PSM_ID); + + assertEquals(1, visits.size()); + assertEquals("Attibele PHC", visits.get(0).getServicePointName()); + assertEquals(VAN_ID, visits.get(0).getVanID()); + assertEquals((short) 1, visits.get(0).getVanSession()); + } + + @Test + @DisplayName("getAvailableVanServicePointMappings should answer nothing when the van visits nowhere") + void getAvailable_shouldAnswerNothingWhenNoVisits() { + when(vanServicePointMappingRepository.getAvailableVanServicePointMappings(PARKING_PLACE_ID, VAN_ID, PSM_ID)) + .thenReturn(new ArrayList<>()); + + assertTrue(service.getAvailableVanServicePointMappings(PARKING_PLACE_ID, VAN_ID, PSM_ID).isEmpty()); + } + + @Test + @DisplayName("getAvailableVanServicePointMappingsV1 should also carry the district and taluk of each visit") + void getAvailableV1_shouldCarryDistrictAndTaluk() { + when(vanServicePointMappingRepository.getAvailableVanServicePointMappingsV1(PARKING_PLACE_ID, VAN_ID, + PSM_ID)).thenReturn(List.of(detailedMappingRow())); + + ArrayList visits = service.getAvailableVanServicePointMappingsV1(PARKING_PLACE_ID, + VAN_ID, PSM_ID); + + assertEquals(1, visits.size()); + assertEquals("Bengaluru Urban", visits.get(0).getDistrictName()); + assertEquals("Anekal", visits.get(0).getBlockName()); + } + + @Test + @DisplayName("getAvailableVanServicePointMappingsV1 should answer nothing when the van visits nowhere") + void getAvailableV1_shouldAnswerNothingWhenNoVisits() { + when(vanServicePointMappingRepository.getAvailableVanServicePointMappingsV1(PARKING_PLACE_ID, VAN_ID, + PSM_ID)).thenReturn(new ArrayList<>()); + + assertTrue(service.getAvailableVanServicePointMappingsV1(PARKING_PLACE_ID, VAN_ID, PSM_ID).isEmpty()); + } + + @Test + @DisplayName("saveVanServicePointMappings should answer the visits the repository stored") + void save_shouldAnswerStoredVisits() { + ArrayList stored = new ArrayList<>(List.of(new M_VanServicePointMap())); + when(vanServicePointMappingRepository.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.saveVanServicePointMappings(new ArrayList<>())); + } + + @Test + @DisplayName("updateVanServicePointMappingStatus should report how many rows the retirement touched") + void updateStatus_shouldReportRowsTouched() { + M_VanServicePointMap request = new M_VanServicePointMap(); + request.setVanServicePointMapID(MAP_ID); + request.setDeleted(Boolean.TRUE); + request.setModifiedBy("admin"); + when(vanServicePointMappingRepository.updateVanServicePointMappingStatus(MAP_ID, Boolean.TRUE, "admin")) + .thenReturn(1); + + assertEquals(1, service.updateVanServicePointMappingStatus(request)); + } + + @Test + @DisplayName("updateVanServicePointMappingStatus should report nothing touched when the visit is unknown") + void updateStatus_shouldReportNothingTouchedForUnknownVisit() { + M_VanServicePointMap request = new M_VanServicePointMap(); + request.setVanServicePointMapID(-1); + when(vanServicePointMappingRepository.updateVanServicePointMappingStatus(-1, null, null)).thenReturn(0); + + assertEquals(0, service.updateVanServicePointMappingStatus(request)); + } + + @Test + @DisplayName("getVanServicePointMappingByID should answer the visit asked for") + void getById_shouldAnswerNamedVisit() { + M_VanServicePointMap stored = new M_VanServicePointMap(); + when(vanServicePointMappingRepository.getVanServicePointMapping(MAP_ID)).thenReturn(stored); + + assertSame(stored, service.getVanServicePointMappingByID(MAP_ID)); + } + + @Test + @DisplayName("getVanServicePointMappingByID should answer nothing when the visit is unknown") + void getById_shouldAnswerNothingForUnknownVisit() { + when(vanServicePointMappingRepository.getVanServicePointMapping(-1)).thenReturn(null); + + assertNull(service.getVanServicePointMappingByID(-1)); + } +} diff --git a/src/test/java/com/iemr/admin/service/vanSpokeMapping/VanSpokeMappingServiceImplTest.java b/src/test/java/com/iemr/admin/service/vanSpokeMapping/VanSpokeMappingServiceImplTest.java new file mode 100644 index 0000000..b538653 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/vanSpokeMapping/VanSpokeMappingServiceImplTest.java @@ -0,0 +1,201 @@ +/* +* 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.admin.service.vanSpokeMapping; + +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.admin.data.VanSpokeMapping.m_VanSpokeMapping; +import com.iemr.admin.repo.VanSpokeMappingRepo.VanSpokeMappingRepo; +import com.iemr.admin.repository.vanMaster.VanMasterRepository; + +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.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The spoke mapping service ties a mobile unit van to the telemedicine spoke it + * serves, and marks the van itself as spoken for while that tie holds. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VanSpokeMappingServiceImpl Test Suite") +class VanSpokeMappingServiceImplTest { + + private static final Integer VAN_ID = 71; + + @Mock + private VanSpokeMappingRepo vanSpokeMappingRepo; + + @Mock + private VanMasterRepository vanMasterRepository; + + @InjectMocks + private VanSpokeMappingServiceImpl service; + + private static m_VanSpokeMapping mapping() { + m_VanSpokeMapping mapping = new m_VanSpokeMapping(); + mapping.setVanspokeID(6001); + mapping.setMmu_VanID(VAN_ID); + mapping.setMmu_parkingPlaceID(31); + mapping.setMmu_servicePointID(88); + mapping.setMmu_vantypeID(2); + mapping.setCreatedBy("admin"); + mapping.setDeleted(Boolean.FALSE); + return mapping; + } + + private static final String SAVE_REQUEST = "{\"vanSpokeMapping\":[{\"mmu_VanID\":71,\"tm_SpokeID\":9," + + "\"createdBy\":\"admin\"}]}"; + + @Test + @DisplayName("saveVanSpokeMapping should record the tie and mark the van as spoken for") + void save_shouldRecordTieAndMarkVanSpokenFor() throws Exception { + when(vanSpokeMappingRepo.saveAll(anyList())).thenReturn(List.of(mapping())); + when(vanMasterRepository.updateVanSpokeMapping(VAN_ID, true, "admin")).thenReturn(1); + + assertEquals("success", service.saveVanSpokeMapping(SAVE_REQUEST)); + verify(vanMasterRepository).updateVanSpokeMapping(VAN_ID, true, "admin"); + } + + @Test + @DisplayName("saveVanSpokeMapping should report failure when the van could not be marked as spoken for") + void save_shouldReportFailureWhenVanNotMarked() throws Exception { + when(vanSpokeMappingRepo.saveAll(anyList())).thenReturn(List.of(mapping())); + when(vanMasterRepository.updateVanSpokeMapping(anyInt(), anyBoolean(), anyString())).thenReturn(0); + + assertEquals("failure", service.saveVanSpokeMapping(SAVE_REQUEST)); + } + + @Test + @DisplayName("saveVanSpokeMapping should report failure when the request carries no mapping at all") + void save_shouldReportFailureWithoutMapping() throws Exception { + assertEquals("failure", service.saveVanSpokeMapping("{\"somethingElse\":1}")); + verify(vanSpokeMappingRepo, never()).saveAll(anyList()); + } + + @Test + @DisplayName("saveVanSpokeMapping should report failure when the request's mapping is empty") + void save_shouldReportFailureForNullMapping() throws Exception { + assertEquals("failure", service.saveVanSpokeMapping("{\"vanSpokeMapping\":null}")); + } + + @Test + @DisplayName("getVanSpokeMappingDetails should answer the ties held at the parking place asked about") + void get_shouldAnswerTiesAtParkingPlace() throws Exception { + when(vanSpokeMappingRepo.getVanSpokeMappingDetails(31, 88, 2)) + .thenReturn(new ArrayList<>(List.of(mapping()))); + + String answered = service.getVanSpokeMappingDetails( + "{\"mmu_parkingplaceID\":31,\"mmu_servicePointId\":88,\"mmu_vanTypeID\":2}"); + + assertTrue(answered.contains("vanSpokeMappedDetails"), answered); + assertTrue(answered.contains("6001"), answered); + } + + @Test + @DisplayName("getVanSpokeMappingDetails should answer an empty holding when the request names nothing") + void get_shouldAnswerEmptyHoldingForEmptyRequest() throws Exception { + assertEquals("{}", service.getVanSpokeMappingDetails("{}")); + } + + @Test + @DisplayName("getVanSpokeMappingDetails should give up when the request leaves out a filter it needs") + void get_shouldGiveUpOnIncompleteRequest() { + assertThrows(RuntimeException.class, + () -> service.getVanSpokeMappingDetails("{\"mmu_parkingplaceID\":31}")); + } + + @Test + @DisplayName("deleteVanSpokeMapping should release the van when the tie is retired") + void delete_shouldReleaseVanWhenTieRetired() throws Exception { + m_VanSpokeMapping retired = mapping(); + retired.setDeleted(Boolean.TRUE); + when(vanSpokeMappingRepo.save(any(m_VanSpokeMapping.class))).thenReturn(retired); + when(vanMasterRepository.updateVanSpokeMapping(VAN_ID, false, "admin")).thenReturn(1); + + assertEquals("success", service.deleteVanSpokeMapping( + "{\"vanSpokeDelete\":{\"vanspokeID\":6001,\"mmu_VanID\":71,\"createdBy\":\"admin\"," + + "\"deleted\":true}}")); + verify(vanMasterRepository).updateVanSpokeMapping(VAN_ID, false, "admin"); + } + + @Test + @DisplayName("deleteVanSpokeMapping should mark the van as spoken for again when the tie is reinstated") + void delete_shouldMarkVanSpokenForWhenTieReinstated() throws Exception { + when(vanSpokeMappingRepo.save(any(m_VanSpokeMapping.class))).thenReturn(mapping()); + when(vanMasterRepository.updateVanSpokeMapping(VAN_ID, true, "admin")).thenReturn(1); + + assertEquals("success", service.deleteVanSpokeMapping( + "{\"vanSpokeDelete\":{\"vanspokeID\":6001,\"mmu_VanID\":71,\"createdBy\":\"admin\"," + + "\"deleted\":false}}")); + verify(vanMasterRepository).updateVanSpokeMapping(VAN_ID, true, "admin"); + } + + @Test + @DisplayName("deleteVanSpokeMapping should report failure when the request carries no tie to retire") + void delete_shouldReportFailureWithoutTie() throws Exception { + assertEquals("failure", service.deleteVanSpokeMapping("{\"somethingElse\":1}")); + verify(vanSpokeMappingRepo, never()).save(any(m_VanSpokeMapping.class)); + } + + @Test + @DisplayName("deleteVanSpokeMapping should report failure when the van could not be released") + void delete_shouldReportFailureWhenVanNotReleased() throws Exception { + when(vanSpokeMappingRepo.save(any(m_VanSpokeMapping.class))).thenReturn(mapping()); + when(vanMasterRepository.updateVanSpokeMapping(anyInt(), anyBoolean(), anyString())).thenReturn(0); + + assertEquals("failure", service.deleteVanSpokeMapping( + "{\"vanSpokeDelete\":{\"vanspokeID\":6001,\"mmu_VanID\":71,\"createdBy\":\"admin\"}}")); + } + + @Test + @DisplayName("updateVanSpokeMapping should record every tie the request carried") + void update_shouldRecordEveryTie() throws Exception { + when(vanSpokeMappingRepo.saveAll(anyList())).thenReturn(new ArrayList<>(List.of(mapping()))); + + assertEquals("success", service.updateVanSpokeMapping("[{\"vanspokeID\":6001,\"mmu_VanID\":71}]")); + } + + @Test + @DisplayName("updateVanSpokeMapping should give up when the request is not a list of ties") + void update_shouldGiveUpOnUnreadableRequest() { + assertThrows(RuntimeException.class, () -> service.updateVanSpokeMapping("{not json")); + } +} diff --git a/src/test/java/com/iemr/admin/service/villageMaster/VillageMasterServiceImplTest.java b/src/test/java/com/iemr/admin/service/villageMaster/VillageMasterServiceImplTest.java new file mode 100644 index 0000000..b55f5ef --- /dev/null +++ b/src/test/java/com/iemr/admin/service/villageMaster/VillageMasterServiceImplTest.java @@ -0,0 +1,150 @@ +/* +* 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.admin.service.villageMaster; + +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.admin.data.locationmaster.DistrictBranchMapping; +import com.iemr.admin.repository.villageMaster.VillageMasterRepository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.when; + +/** + * The village service keeps the villages of a taluk, along with the panchayat, + * habitation and pin code each one sits in. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("VillageMasterServiceImpl Test Suite") +class VillageMasterServiceImplTest { + + private static final Integer BLOCK_ID = 3011; + private static final Integer VILLAGE_ID = 30111; + + @Mock + private VillageMasterRepository villageMasterRepository; + + @InjectMocks + private VillageMasterServiceImpl service; + + private static Object[] villageRow() { + return new Object[] { VILLAGE_ID, BLOCK_ID, "Anekal", "Attibele panchayat", "Attibele", "Main habitation", + "562107", 900111, 900011, Boolean.FALSE, Boolean.TRUE }; + } + + private static DistrictBranchMapping village() { + DistrictBranchMapping village = new DistrictBranchMapping(); + village.setDistrictBranchID(VILLAGE_ID); + village.setVillageName("Attibele"); + village.setPanchayatName("Attibele panchayat"); + village.setHabitat("Main habitation"); + village.setPinCode("562107"); + village.setGovVillageID(900111); + village.setGovSubDistrictID(900011); + village.setIsRural(Boolean.TRUE); + village.setModifiedBy("admin"); + return village; + } + + @Test + @DisplayName("storeVillageDetails should answer the villages the repository stored") + void store_shouldAnswerStoredVillages() { + ArrayList stored = new ArrayList<>(List.of(village())); + when(villageMasterRepository.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.storeVillageDetails(new ArrayList<>())); + } + + @Test + @DisplayName("getAvailableVillages should rebuild one village per row the query answers") + void getAvailable_shouldRebuildEachRow() { + when(villageMasterRepository.getAvailableVillages(BLOCK_ID)).thenReturn(List.of(villageRow())); + + ArrayList villages = service.getAvailableVillages(BLOCK_ID); + + assertEquals(1, villages.size()); + assertEquals("Attibele", villages.get(0).getVillageName()); + assertEquals("Attibele panchayat", villages.get(0).getPanchayatName()); + assertEquals("562107", villages.get(0).getPinCode()); + assertEquals(Boolean.TRUE, villages.get(0).getIsRural()); + } + + @Test + @DisplayName("getAvailableVillages should answer nothing when the taluk holds no village") + void getAvailable_shouldAnswerNothingForEmptyTaluk() { + when(villageMasterRepository.getAvailableVillages(BLOCK_ID)).thenReturn(new ArrayList<>()); + + assertTrue(service.getAvailableVillages(BLOCK_ID).isEmpty()); + } + + @Test + @DisplayName("updateVillageStatus should report how many villages the retirement touched") + void updateStatus_shouldReportRowsTouched() { + DistrictBranchMapping request = village(); + request.setDeleted(Boolean.TRUE); + when(villageMasterRepository.updateVillageStatus(VILLAGE_ID, Boolean.TRUE, "admin")).thenReturn(1); + + assertEquals(1, service.updateVillageStatus(request)); + } + + @Test + @DisplayName("updateVillageStatus should report nothing touched when the village is unknown") + void updateStatus_shouldReportNothingTouchedForUnknownVillage() { + DistrictBranchMapping request = new DistrictBranchMapping(); + request.setDistrictBranchID(-1); + when(villageMasterRepository.updateVillageStatus(-1, null, null)).thenReturn(0); + + assertEquals(0, service.updateVillageStatus(request)); + } + + @Test + @DisplayName("updateVillageData should carry every changed detail through to the repository") + void updateData_shouldCarryEveryChangedDetail() { + when(villageMasterRepository.updateVillageData("Attibele panchayat", "Attibele", "Main habitation", "562107", + 900111, 900011, VILLAGE_ID, "admin", Boolean.TRUE)).thenReturn(1); + + assertEquals(1, service.updateVillageData(village())); + } + + @Test + @DisplayName("updateVillageData should report nothing touched when the village is unknown") + void updateData_shouldReportNothingTouchedForUnknownVillage() { + DistrictBranchMapping request = village(); + request.setDistrictBranchID(-1); + + assertEquals(0, service.updateVillageData(request)); + } +} diff --git a/src/test/java/com/iemr/admin/service/zonemaster/ZoneMasterServiceImplTest.java b/src/test/java/com/iemr/admin/service/zonemaster/ZoneMasterServiceImplTest.java new file mode 100644 index 0000000..aa421d6 --- /dev/null +++ b/src/test/java/com/iemr/admin/service/zonemaster/ZoneMasterServiceImplTest.java @@ -0,0 +1,181 @@ +/* +* 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.admin.service.zonemaster; + +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.admin.data.zonemaster.M_Zone; +import com.iemr.admin.data.zonemaster.M_ZoneDistrictMap; +import com.iemr.admin.repository.zonemaster.ZoneDistrictMappingRepo; +import com.iemr.admin.repository.zonemaster.ZoneMasterRepository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +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; + +/** + * The zone service rebuilds zones and their district mappings out of positional + * query results, and cascades a retired zone onto the districts under it. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("ZoneMasterServiceImpl Test Suite") +class ZoneMasterServiceImplTest { + + private static final Integer PSM_ID = 4001; + private static final Integer ZONE_ID = 61; + + @Mock + private ZoneMasterRepository zoneMasterRepo; + + @Mock + private ZoneDistrictMappingRepo zoneDistrictMappingRepo; + + @InjectMocks + private ZoneMasterServiceImpl service; + + private static Object[] zoneRow() { + return new Object[] { ZONE_ID, "North zone", "Northern districts", "Main Road", PSM_ID, Boolean.FALSE, + 29, "Karnataka", 301, "Bengaluru Urban", 401, "North block", 501, "Hosur", 1, "India", null, + 91, "N" }; + } + + private static Object[] mappingRow() { + return new Object[] { 9001, ZONE_ID, "North zone", 301, PSM_ID, Boolean.FALSE, 29, "Karnataka", + "Bengaluru Urban", 1, "N", Boolean.FALSE }; + } + + @Test + @DisplayName("createZone should hand its batch to the repository") + void createZone_shouldHandBatchToRepository() { + ArrayList stored = new ArrayList<>(List.of(new M_Zone())); + when(zoneMasterRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.createZone(new ArrayList<>())); + } + + @Test + @DisplayName("getAvailableZones should rebuild one zone per row the query answers") + void getAvailableZones_shouldRebuildEachRow() { + when(zoneMasterRepo.getAvailableZones(PSM_ID)).thenReturn(List.of(zoneRow())); + + ArrayList zones = service.getAvailableZones(PSM_ID); + + assertEquals(1, zones.size()); + assertEquals("North zone", zones.get(0).getZoneName()); + } + + @Test + @DisplayName("createZoneDistrictMapping should hand its batch to the repository") + void createZoneDistrictMapping_shouldHandBatchToRepository() { + ArrayList stored = new ArrayList<>(List.of(new M_ZoneDistrictMap())); + when(zoneDistrictMappingRepo.saveAll(anyList())).thenReturn(stored); + + assertSame(stored, service.createZoneDistrictMapping(new ArrayList<>())); + } + + @Test + @DisplayName("getAvailableZoneDistrictMappings should rebuild one mapping per row the query answers") + void getAvailableZoneDistrictMappings_shouldRebuildEachRow() { + when(zoneDistrictMappingRepo.getAvailableZoneDistrictMappings(PSM_ID)) + .thenReturn(List.of(mappingRow())); + + assertEquals(1, service.getAvailableZoneDistrictMappings(PSM_ID).size()); + } + + @Test + @DisplayName("updateZoneStatus should carry the change onto the districts under the zone") + void updateZoneStatus_shouldCascadeToDistricts() { + M_Zone request = new M_Zone(ZONE_ID, "North zone", null, null, PSM_ID, Boolean.TRUE, 1, "India", + 29, "Karnataka", 301, "Bengaluru Urban", 401, "North block", 501, "Hosur", null, 3, + "Tele Medicine"); + request.setModifiedBy("admin"); + when(zoneMasterRepo.updateZoneStatus(ZONE_ID, Boolean.TRUE, "admin")).thenReturn(1); + when(zoneDistrictMappingRepo.getAvailableZoneDistrictMappingss("61")) + .thenReturn(List.of(mappingRow())); + when(zoneDistrictMappingRepo.updateZoneDistrictMappingStatus(anyInt(), org.mockito.ArgumentMatchers.any(), + anyString())).thenReturn(1); + + assertEquals(1, service.updateZoneStatus(request)); + verify(zoneDistrictMappingRepo).updateZoneDistrictMappingStatus(9001, Boolean.TRUE, "admin"); + } + + @Test + @DisplayName("updateZoneDistrictMappingStatus should reach the repository query") + void updateZoneDistrictMappingStatus_shouldReachRepository() { + M_ZoneDistrictMap request = new M_ZoneDistrictMap(9001, ZONE_ID, "North zone", 301, PSM_ID, + Boolean.TRUE, 29, "Karnataka", "Bengaluru Urban", 3, "Tele Medicine", Boolean.FALSE); + request.setModifiedBy("admin"); + when(zoneDistrictMappingRepo.updateZoneDistrictMappingStatus(9001, Boolean.TRUE, "admin")).thenReturn(1); + + assertEquals(1, service.updateZoneDistrictMappingStatus(request)); + } + + @Test + @DisplayName("the remaining calls should each reach their own repository query") + void remainingCalls_shouldReachTheirOwnQuery() { + M_Zone zone = new M_Zone(); + M_ZoneDistrictMap mapping = new M_ZoneDistrictMap(); + when(zoneMasterRepo.save(zone)).thenReturn(zone); + when(zoneMasterRepo.getZoneById(ZONE_ID)).thenReturn(zone); + when(zoneDistrictMappingRepo.findByZoneDistrictMapID(9001)).thenReturn(mapping); + when(zoneDistrictMappingRepo.save(mapping)).thenReturn(mapping); + + assertSame(zone, service.updateZoneData(zone)); + assertSame(zone, service.getzoneByID(ZONE_ID)); + assertSame(mapping, service.editZoneDistrictMapping(9001)); + assertSame(mapping, service.saveeditedData(mapping)); + } + + @Test + @DisplayName("editZoneDistrictMapping1 should rebuild one district per row the query answers") + void editZoneDistrictMapping1_shouldRebuildEachRow() { + when(zoneDistrictMappingRepo.editZoneDistrictMapping1(ZONE_ID)) + .thenReturn(List.of(new Object[] { "Bengaluru Urban", 301 })); + + assertEquals(1, service.editZoneDistrictMapping1(ZONE_ID).size()); + } + + @Test + @DisplayName("getAllMappedRecord should report whether the zone maps more than one district") + void getAllMappedRecord_shouldReportWhetherZoneMapsMany() { + when(zoneDistrictMappingRepo.getRecord(ZONE_ID)).thenReturn(List.of(new Object(), new Object())); + assertEquals(100, service.getAllMappedRecord(ZONE_ID)); + + when(zoneDistrictMappingRepo.getRecord(ZONE_ID)).thenReturn(List.of(new Object())); + assertEquals(200, service.getAllMappedRecord(ZONE_ID)); + } +} diff --git a/src/test/java/com/iemr/admin/sevice/labmodule/LabModuleServicesTest.java b/src/test/java/com/iemr/admin/sevice/labmodule/LabModuleServicesTest.java new file mode 100644 index 0000000..2d3c540 --- /dev/null +++ b/src/test/java/com/iemr/admin/sevice/labmodule/LabModuleServicesTest.java @@ -0,0 +1,613 @@ +/* +* 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.admin.sevice.labmodule; + +import java.sql.Timestamp; +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.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 com.iemr.admin.data.calibration.Calibration; +import com.iemr.admin.data.labmodule.ComponentMaster; +import com.iemr.admin.data.labmodule.ComponentResultMap; +import com.iemr.admin.data.labmodule.IOTComponent; +import com.iemr.admin.data.labmodule.IOTProcedure; +import com.iemr.admin.data.labmodule.ProcedureComponentMapping; +import com.iemr.admin.data.labmodule.ProcedureMaster; +import com.iemr.admin.repo.calibration.CalibrationAPIRepo; +import com.iemr.admin.repo.labmodule.ComponentMasterRepo; +import com.iemr.admin.repo.labmodule.ComponentResultMapRepo; +import com.iemr.admin.repo.labmodule.IOTRepo; +import com.iemr.admin.repo.labmodule.ProcedureComponentMappingRepo; +import com.iemr.admin.repo.labmodule.ProcedureMasterRepo; +import com.iemr.admin.utils.exception.IEMRException; + +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.junit.jupiter.api.Assertions.assertTrue; +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.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; + +/** + * The lab module masters define the tests a diagnostic device can run and the + * components each test reports, so a broken mapping means a result that cannot + * be recorded against its test. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("Lab module master service Test Suite") +class LabModuleServicesTest { + + private static final Integer PSM_ID = 4001; + private static final Integer PROCEDURE_ID = 71; + private static final Integer COMPONENT_ID = 81; + + @Mock + private ProcedureMasterRepo procedureMasterRepo; + + @Mock + private ComponentMasterRepo componentMasterRepo; + + @Mock + private ComponentResultMapRepo componentResultMapRepo; + + @Mock + private ProcedureComponentMappingRepo procedureComponentMappingRepo; + + @Mock + private CalibrationAPIRepo calibrationAPIRepo; + + @Mock + private IOTRepo iotRepo; + + private MastersCreationServiceImpl creationService; + private MastersFetchingServiceImpl fetchingService; + private MastersMappingServiceImpl mappingService; + private MastersStatusUpdateImpl statusService; + private IOTServiceImpl iotService; + + @BeforeEach + void setUp() { + creationService = new MastersCreationServiceImpl(); + creationService.setProcedureMasterRepo(procedureMasterRepo); + creationService.setComponentMasterRepo(componentMasterRepo); + creationService.setComponentResultMapRepo(componentResultMapRepo); + creationService.calibrationAPIRepo = calibrationAPIRepo; + creationService.iotRepo = iotRepo; + + fetchingService = new MastersFetchingServiceImpl(); + fetchingService.setProcedureMasterRepo(procedureMasterRepo); + fetchingService.setComponentMasterRepo(componentMasterRepo); + fetchingService.setComponentResultMapRepo(componentResultMapRepo); + fetchingService.setProcedureComponentMappingRepo(procedureComponentMappingRepo); + + mappingService = new MastersMappingServiceImpl(); + mappingService.setProcedureComponentMappingRepo(procedureComponentMappingRepo); + + statusService = new MastersStatusUpdateImpl(); + statusService.setProcedureMasterRepo(procedureMasterRepo); + statusService.setComponentMasterRepo(componentMasterRepo); + statusService.setComponentResultMapRepo(componentResultMapRepo); + statusService.calibrationAPIRepo = calibrationAPIRepo; + statusService.iotRepo = iotRepo; + + iotService = new IOTServiceImpl(); + org.springframework.test.util.ReflectionTestUtils.setField(iotService, "iotRepo", iotRepo); + } + + /** One row of the procedure detail query, in the order the builder reads it. */ + private static ArrayList procedureDetailRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { PROCEDURE_ID, "Haemoglobin", "Blood test", "Lab", "Both", PSM_ID, Boolean.FALSE, + "N", "admin", Timestamp.valueOf("2026-02-17 09:30:00"), "admin", Boolean.FALSE, + Timestamp.valueOf("2026-02-17 09:30:00") }); + return rows; + } + + private static ArrayList componentDetailRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { COMPONENT_ID, "Haemoglobin count", "g/dL", "TextBox", "LOINC-1", Boolean.FALSE }); + return rows; + } + + @Nested + @DisplayName("MastersCreationServiceImpl") + class CreationTests { + + @Test + @DisplayName("createProcedureMaster should default a procedure that does not say whether it is mandatory") + void createProcedureMaster_shouldDefaultMandatoryFlag() throws Exception { + when(procedureMasterRepo.save(any())).thenAnswer(call -> { + ProcedureMaster saved = call.getArgument(0); + saved.setProcedureID(PROCEDURE_ID); + return saved; + }); + when(procedureMasterRepo.getProcedureDetails(PROCEDURE_ID)).thenReturn(procedureDetailRow()); + + String created = creationService.createProcedureMaster( + "{\"procedureName\":\"Haemoglobin\",\"procedureType\":\"Lab\",\"createdBy\":\"admin\"}"); + + assertTrue(created.contains("Haemoglobin"), created); + } + + @Test + @DisplayName("createProcedureMaster should answer nothing when the stored procedure cannot be read back") + void createProcedureMaster_shouldAnswerNothingWhenUnreadable() throws Exception { + when(procedureMasterRepo.save(any())).thenAnswer(call -> call.getArgument(0)); + when(procedureMasterRepo.getProcedureDetails(any())).thenReturn(new ArrayList<>()); + + assertNull(creationService.createProcedureMaster("{\"procedureName\":\"Haemoglobin\"}")); + } + + @Test + @DisplayName("createProcedureMaster should fill the calibration URLs for a calibrated device test") + void createProcedureMaster_shouldFillCalibrationUrls() throws Exception { + IOTProcedure iotProcedure = new IOTProcedure(); + iotProcedure.setCalibrationCode("HB"); + Calibration calibration = new Calibration(); + calibration.setCalibrationStartAPI("http://device/start/{test_name}"); + calibration.setCalibrationStatusAPI("http://device/status/{test_name}"); + calibration.setCalibrationEndAPI("http://device/end/{test_name}"); + when(iotRepo.getIOTProcedureByID(91)).thenReturn(iotProcedure); + when(calibrationAPIRepo.getCalibration()).thenReturn(calibration); + when(procedureMasterRepo.save(any())).thenAnswer(call -> call.getArgument(0)); + when(procedureMasterRepo.getProcedureDetails(any())).thenReturn(procedureDetailRow()); + + creationService.createProcedureMaster("{\"procedureName\":\"Haemoglobin\"," + + "\"isCalibration\":true,\"iotProcedureID\":91}"); + + verify(iotRepo).updateIOTWithCalibration("http://device/start/HB", "http://device/status/HB", + "http://device/end/HB", 91); + } + + @Test + @DisplayName("createProcedureMaster should leave the calibration URLs alone once they are set") + void createProcedureMaster_shouldLeaveExistingCalibrationUrls() throws Exception { + IOTProcedure iotProcedure = new IOTProcedure(); + iotProcedure.setCalibrationCode("HB"); + iotProcedure.setCalibrationStartAPI("http://device/start/HB"); + when(iotRepo.getIOTProcedureByID(91)).thenReturn(iotProcedure); + when(calibrationAPIRepo.getCalibration()).thenReturn(new Calibration()); + when(procedureMasterRepo.save(any())).thenAnswer(call -> call.getArgument(0)); + when(procedureMasterRepo.getProcedureDetails(any())).thenReturn(procedureDetailRow()); + + creationService.createProcedureMaster("{\"procedureName\":\"Haemoglobin\"," + + "\"isCalibration\":true,\"iotProcedureID\":91}"); + + verify(iotRepo, never()).updateIOTWithCalibration(anyString(), anyString(), anyString(), anyInt()); + } + + @Test + @DisplayName("createProcedureMaster should refuse a device the calibration store does not know") + void createProcedureMaster_shouldRefuseUnknownDevice() { + when(iotRepo.getIOTProcedureByID(91)).thenReturn(null); + + assertThrows(IEMRException.class, () -> creationService.createProcedureMaster( + "{\"procedureName\":\"Haemoglobin\",\"isCalibration\":true,\"iotProcedureID\":91}")); + } + + @Test + @DisplayName("createComponentMaster should store the result options a picklist component offers") + void createComponentMaster_shouldStoreResultOptions() throws Exception { + when(componentMasterRepo.save(any())).thenAnswer(call -> call.getArgument(0)); + when(componentResultMapRepo.saveAll(anyList())).thenAnswer(call -> { + List given = call.getArgument(0); + return new ArrayList<>(given); + }); + + String created = creationService.createComponentMaster("{\"testComponentName\":\"Blood group\"," + + "\"testComponentID\":81,\"providerServiceMapID\":4001,\"createdBy\":\"admin\"," + + "\"compOpt\":[{\"name\":\"A+\"},{\"name\":\"B+\"}]}"); + + assertTrue(created.contains("Blood group"), created); + } + + @Test + @DisplayName("createComponentMaster should store a component that offers no result options") + void createComponentMaster_shouldStoreComponentWithoutOptions() throws Exception { + when(componentMasterRepo.save(any())).thenAnswer(call -> call.getArgument(0)); + + String created = creationService + .createComponentMaster("{\"testComponentName\":\"Haemoglobin count\"}"); + + assertTrue(created.contains("Haemoglobin count"), created); + verify(componentResultMapRepo, never()).saveAll(anyList()); + } + + @Test + @DisplayName("createComponentMaster should refuse a run that stored fewer options than it was given") + void createComponentMaster_shouldRefusePartialOptionStore() { + when(componentMasterRepo.save(any())).thenAnswer(call -> call.getArgument(0)); + when(componentResultMapRepo.saveAll(anyList())).thenReturn(new ArrayList()); + + assertThrows(Exception.class, () -> creationService.createComponentMaster( + "{\"testComponentName\":\"Blood group\",\"compOpt\":[{\"name\":\"A+\"}]}")); + } + + @Test + @DisplayName("createComponentMaster should answer nothing when the component could not be stored") + void createComponentMaster_shouldAnswerNothingWhenUnstored() throws Exception { + when(componentMasterRepo.save(any())).thenReturn(null); + + assertNull(creationService.createComponentMaster("{\"testComponentName\":\"Blood group\"}")); + } + } + + @Nested + @DisplayName("MastersFetchingServiceImpl") + class FetchingTests { + + @Test + @DisplayName("getProcedureMaster should publish the procedures of the provider") + void getProcedureMaster_shouldPublishProviderProcedures() throws Exception { + ProcedureMaster procedure = new ProcedureMaster(); + procedure.setProcedureID(PROCEDURE_ID); + procedure.setProcedureName("Haemoglobin"); + when(procedureMasterRepo.findProcByPSMIDc(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(procedure))); + + assertTrue(fetchingService.getProcedureMaster(PSM_ID).contains("Haemoglobin")); + } + + @Test + @DisplayName("getProcedureMasterDelFalse should publish only the live procedures") + void getProcedureMasterDelFalse_shouldPublishLiveProcedures() throws Exception { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { PROCEDURE_ID, "Haemoglobin", "Blood test", "Lab" }); + when(procedureMasterRepo.getProcedureDetailsDelFalse(PSM_ID)).thenReturn(rows); + + assertTrue(fetchingService.getProcedureMasterDelFalse(PSM_ID).contains("Haemoglobin")); + } + + @Test + @DisplayName("getComponentMaster should publish the components of the provider") + void getComponentMaster_shouldPublishProviderComponents() throws Exception { + ComponentMaster component = new ComponentMaster(); + component.setTestComponentID(COMPONENT_ID); + component.setTestComponentName("Haemoglobin count"); + when(componentMasterRepo.getComponentDetailsBypsmID(PSM_ID)) + .thenReturn(new ArrayList<>(List.of(component))); + + assertTrue(fetchingService.getComponentMaster(PSM_ID).contains("Haemoglobin count")); + } + + @Test + @DisplayName("getComponentMasterDelFalse should publish only the live components") + void getComponentMasterDelFalse_shouldPublishLiveComponents() throws Exception { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { COMPONENT_ID, "Haemoglobin count", "g/dL", "TextBox", "LOINC-1", "component" }); + when(componentMasterRepo.getComponentDetailsDelFalse(PSM_ID)).thenReturn(rows); + + assertTrue(fetchingService.getComponentMasterDelFalse(PSM_ID).contains("Haemoglobin count")); + } + + @Test + @DisplayName("getComponentMasterDelFalse should publish an empty list when nothing is live") + void getComponentMasterDelFalse_shouldPublishEmptyList() throws Exception { + when(componentMasterRepo.getComponentDetailsDelFalse(PSM_ID)).thenReturn(new ArrayList<>()); + + assertEquals("[]", fetchingService.getComponentMasterDelFalse(PSM_ID)); + } + + @Test + @DisplayName("getProcCompMappingDelFalse should publish the mappings of the provider") + void getProcCompMappingDelFalse_shouldPublishMappings() throws Exception { + when(procedureComponentMappingRepo.getProcedureComponentMappingList(PSM_ID)) + .thenReturn(mappingRow()); + + assertTrue(fetchingService.getProcCompMappingDelFalse(PSM_ID).contains("Haemoglobin")); + } + + @Test + @DisplayName("getProcCompMappingDelFalse should publish an empty list when nothing is mapped") + void getProcCompMappingDelFalse_shouldPublishEmptyList() throws Exception { + when(procedureComponentMappingRepo.getProcedureComponentMappingList(PSM_ID)) + .thenReturn(new ArrayList<>()); + + assertEquals("[]", fetchingService.getProcCompMappingDelFalse(PSM_ID)); + } + + @Test + @DisplayName("getProcCompMappingForProcedureID should publish the mappings of the procedure") + void getProcCompMappingForProcedureID_shouldPublishMappings() throws Exception { + when(procedureComponentMappingRepo.getProcedureComponentMappingListForProcedureID(PROCEDURE_ID)) + .thenReturn(mappingRow()); + + assertTrue(fetchingService.getProcCompMappingForProcedureID(PROCEDURE_ID).contains("Haemoglobin")); + } + + @Test + @DisplayName("getProcCompMappingForProcedureID should publish an empty list when nothing is mapped") + void getProcCompMappingForProcedureID_shouldPublishEmptyList() throws Exception { + when(procedureComponentMappingRepo.getProcedureComponentMappingListForProcedureID(PROCEDURE_ID)) + .thenReturn(new ArrayList<>()); + + assertEquals("[]", fetchingService.getProcCompMappingForProcedureID(PROCEDURE_ID)); + } + + @Test + @DisplayName("getComponentDetailsForComponentID should publish a typed component without its options") + void getComponentDetails_shouldPublishTypedComponent() throws Exception { + ComponentMaster stored = new ComponentMaster(); + stored.setTestComponentID(COMPONENT_ID); + stored.setTestComponentName("Haemoglobin count"); + stored.setInputType("TextBox"); + when(componentMasterRepo.findByTestComponentID(COMPONENT_ID)).thenReturn(stored); + + String published = fetchingService.getComponentDetailsForComponentID(COMPONENT_ID); + + assertTrue(published.contains("Haemoglobin count"), published); + verify(componentResultMapRepo, never()).findByTestComponentIDAndDeleted(anyInt(), anyBoolean()); + } + + @Test + @DisplayName("getComponentDetailsForComponentID should publish a picklist component with its options") + void getComponentDetails_shouldPublishPicklistWithOptions() throws Exception { + ComponentMaster stored = new ComponentMaster(); + stored.setTestComponentID(COMPONENT_ID); + stored.setTestComponentName("Blood group"); + stored.setInputType("Dropdown"); + ComponentResultMap option = new ComponentResultMap(); + option.setResultValue("A+"); + when(componentMasterRepo.findByTestComponentID(COMPONENT_ID)).thenReturn(stored); + when(componentResultMapRepo.findByTestComponentIDAndDeleted(COMPONENT_ID, false)) + .thenReturn(new ArrayList<>(List.of(option))); + + String published = fetchingService.getComponentDetailsForComponentID(COMPONENT_ID); + + assertTrue(published.contains("A+"), published); + } + + @Test + @DisplayName("getComponentDetailsForComponentID should answer nothing for a component that does not exist") + void getComponentDetails_shouldAnswerNothingForUnknownComponent() throws Exception { + when(componentMasterRepo.findByTestComponentID(COMPONENT_ID)).thenReturn(null); + + assertNull(fetchingService.getComponentDetailsForComponentID(COMPONENT_ID)); + } + + private ArrayList mappingRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { PROCEDURE_ID, COMPONENT_ID, "Haemoglobin", "Blood test", + "Haemoglobin count", null, "g/dL" }); + return rows; + } + } + + @Nested + @DisplayName("MastersMappingServiceImpl") + class MappingTests { + + @Test + @DisplayName("createProcedureComponentMapping should replace the previous mapping of the procedure") + void createMapping_shouldReplacePreviousMapping() throws Exception { + when(procedureComponentMappingRepo.saveAll(anyList())) + .thenAnswer(call -> new ArrayList<>((List) call.getArgument(0))); + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { PROCEDURE_ID, COMPONENT_ID, "Haemoglobin", "Blood test", + "Haemoglobin count", null, "g/dL" }); + when(procedureComponentMappingRepo.getProcedureComponentMappingListForProcedureID(PROCEDURE_ID)) + .thenReturn(rows); + + String mapped = mappingService.createProcedureComponentMapping("{\"procedureID\":71," + + "\"providerServiceMapID\":4001,\"createdBy\":\"admin\"," + + "\"compList\":[{\"testComponentID\":81}]}"); + + assertTrue(mapped.contains("Haemoglobin"), mapped); + verify(procedureComponentMappingRepo).softDeleteProcCompMapping(PROCEDURE_ID, "admin"); + } + + @Test + @DisplayName("createProcedureComponentMapping should answer nothing when it stored fewer than it was given") + void createMapping_shouldAnswerNothingOnPartialStore() throws Exception { + when(procedureComponentMappingRepo.saveAll(anyList())) + .thenReturn(new ArrayList()); + + assertNull(mappingService.createProcedureComponentMapping("{\"procedureID\":71," + + "\"createdBy\":\"admin\",\"compList\":[{\"testComponentID\":81}]}")); + } + + @Test + @DisplayName("createProcedureComponentMapping should report a request that maps no components") + void createMapping_shouldReportEmptyRequest() throws Exception { + assertEquals("1", mappingService.createProcedureComponentMapping("{\"procedureID\":71}")); + verify(procedureComponentMappingRepo, never()).saveAll(anyList()); + } + } + + @Nested + @DisplayName("MastersStatusUpdateImpl") + class StatusUpdateTests { + + @Test + @DisplayName("updateProcedureStatus should publish the procedure once its status has changed") + void updateProcedureStatus_shouldPublishChangedProcedure() throws Exception { + when(procedureMasterRepo.updateProcedureStatus(PROCEDURE_ID, true)).thenReturn(1); + when(procedureMasterRepo.getProcedureDetails(PROCEDURE_ID)).thenReturn(procedureDetailRow()); + + assertTrue(statusService.updateProcedureStatus(PROCEDURE_ID, true).contains("Haemoglobin")); + } + + @Test + @DisplayName("updateProcedureStatus should answer nothing when no procedure changed") + void updateProcedureStatus_shouldAnswerNothingWhenNothingChanged() throws Exception { + when(procedureMasterRepo.updateProcedureStatus(PROCEDURE_ID, true)).thenReturn(0); + + assertNull(statusService.updateProcedureStatus(PROCEDURE_ID, true)); + } + + @Test + @DisplayName("updateComponentStatus should publish the component once its status has changed") + void updateComponentStatus_shouldPublishChangedComponent() throws Exception { + when(componentMasterRepo.updateComponentStatus(COMPONENT_ID, true)).thenReturn(1); + when(componentMasterRepo.getComponentDetailsByCompID(COMPONENT_ID)).thenReturn(componentDetailRow()); + + assertTrue(statusService.updateComponentStatus(COMPONENT_ID, true).contains("Haemoglobin count")); + } + + @Test + @DisplayName("updateComponentStatus should answer nothing when no component changed") + void updateComponentStatus_shouldAnswerNothingWhenNothingChanged() throws Exception { + when(componentMasterRepo.updateComponentStatus(COMPONENT_ID, true)).thenReturn(0); + + assertNull(statusService.updateComponentStatus(COMPONENT_ID, true)); + } + + @Test + @DisplayName("updateProcedureMaster should publish the procedure once the edit lands") + void updateProcedureMaster_shouldPublishEditedProcedure() throws Exception { + when(procedureMasterRepo.updateProcedureDetails(anyInt(), anyString(), any(), anyString(), + anyString(), anyString(), any(), any(), any())).thenReturn(1); + when(procedureMasterRepo.getProcedureDetails(PROCEDURE_ID)).thenReturn(procedureDetailRow()); + + String published = statusService.updateProcedureMaster("{\"procedureID\":71," + + "\"procedureName\":\"Haemoglobin\",\"procedureType\":\"Lab\",\"gender\":\"Both\"," + + "\"modifiedBy\":\"admin\"}"); + + assertTrue(published.contains("Haemoglobin"), published); + } + + @Test + @DisplayName("updateProcedureMaster should answer nothing for an edit that leaves out a mandatory field") + void updateProcedureMaster_shouldAnswerNothingForIncompleteEdit() throws Exception { + assertNull(statusService.updateProcedureMaster("{\"procedureID\":71}")); + } + + @Test + @DisplayName("updateProcedureMaster should clear the calibration URLs when calibration is switched off") + void updateProcedureMaster_shouldClearCalibrationUrls() throws Exception { + IOTProcedure iotProcedure = new IOTProcedure(); + iotProcedure.setCalibrationCode("HB"); + iotProcedure.setCalibrationStartAPI("http://device/start/HB"); + when(iotRepo.getIOTProcedureByID(91)).thenReturn(iotProcedure); + when(calibrationAPIRepo.getCalibration()).thenReturn(new Calibration()); + when(procedureMasterRepo.updateProcedureDetails(anyInt(), anyString(), any(), anyString(), + anyString(), anyString(), any(), any(), any())).thenReturn(1); + when(procedureMasterRepo.getProcedureDetails(PROCEDURE_ID)).thenReturn(procedureDetailRow()); + + statusService.updateProcedureMaster("{\"procedureID\":71,\"procedureName\":\"Haemoglobin\"," + + "\"procedureType\":\"Lab\",\"gender\":\"Both\",\"modifiedBy\":\"admin\"," + + "\"isCalibration\":false,\"iotProcedureID\":91}"); + + verify(iotRepo).updateIOTWithCalibration(null, null, null, 91); + } + + @Test + @DisplayName("updateProcedureMaster should refuse a device the calibration store does not know") + void updateProcedureMaster_shouldRefuseUnknownDevice() { + when(iotRepo.getIOTProcedureByID(91)).thenReturn(null); + + assertThrows(IEMRException.class, () -> statusService.updateProcedureMaster( + "{\"procedureID\":71,\"isCalibration\":true,\"iotProcedureID\":91}")); + } + + @Test + @DisplayName("updateComponentMaster should publish a typed component once the edit lands") + void updateComponentMaster_shouldPublishEditedTypedComponent() throws Exception { + when(componentMasterRepo.updateComponentDetailsTextBox(anyInt(), anyString(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(1); + when(componentMasterRepo.getComponentDetailsByCompID(COMPONENT_ID)).thenReturn(componentDetailRow()); + + String published = statusService.updateComponentMaster("{\"testComponentID\":81," + + "\"testComponentName\":\"Haemoglobin count\",\"inputType\":\"TextBox\"," + + "\"modifiedBy\":\"admin\"}"); + + assertTrue(published.contains("Haemoglobin count"), published); + } + + @Test + @DisplayName("updateComponentMaster should replace the result options of a picklist component") + void updateComponentMaster_shouldReplacePicklistOptions() throws Exception { + when(componentMasterRepo.updateComponentDetailsOtherThenTextBox(anyInt(), anyString(), any(), + anyString(), any(), any(), any())).thenReturn(1); + when(componentResultMapRepo.saveAll(anyList())) + .thenAnswer(call -> new ArrayList<>((List) call.getArgument(0))); + when(componentMasterRepo.getComponentDetailsByCompID(COMPONENT_ID)).thenReturn(componentDetailRow()); + + String published = statusService.updateComponentMaster("{\"testComponentID\":81," + + "\"testComponentName\":\"Blood group\",\"inputType\":\"Dropdown\"," + + "\"modifiedBy\":\"admin\",\"compOpt\":[{\"name\":\"A+\"}]}"); + + assertTrue(published.contains("Haemoglobin count"), published); + verify(componentResultMapRepo).deletePreviousCompResultMappingSoft(COMPONENT_ID, "admin"); + } + + @Test + @DisplayName("updateComponentMaster should refuse a run that stored fewer options than it was given") + void updateComponentMaster_shouldRefusePartialOptionStore() { + when(componentMasterRepo.updateComponentDetailsOtherThenTextBox(anyInt(), anyString(), any(), + anyString(), any(), any(), any())).thenReturn(1); + when(componentResultMapRepo.saveAll(anyList())).thenReturn(new ArrayList()); + + assertThrows(Exception.class, () -> statusService.updateComponentMaster("{\"testComponentID\":81," + + "\"testComponentName\":\"Blood group\",\"inputType\":\"Dropdown\"," + + "\"modifiedBy\":\"admin\",\"compOpt\":[{\"name\":\"A+\"}]}")); + } + + @Test + @DisplayName("updateComponentMaster should answer nothing for a component that names no input type") + void updateComponentMaster_shouldAnswerNothingWithoutInputType() throws Exception { + assertNull(statusService.updateComponentMaster("{\"testComponentID\":81}")); + } + } + + @Nested + @DisplayName("IOTServiceImpl") + class IotServiceTests { + + @Test + @DisplayName("getIOTProcedure should answer the device tests on record") + void getIOTProcedure_shouldAnswerDeviceTests() { + IOTProcedure procedure = new IOTProcedure(); + procedure.setCalibrationCode("HB"); + when(iotRepo.getIOTProcedure()).thenReturn(new ArrayList<>(List.of(procedure))); + + assertTrue(iotService.getIOTProcedure().contains("HB")); + } + + @Test + @DisplayName("getIOTComponent should answer the device components on record") + void getIOTComponent_shouldAnswerDeviceComponents() { + IOTComponent component = new IOTComponent(); + when(iotRepo.getIOTComponent()).thenReturn(new ArrayList<>(List.of(component))); + + assertTrue(iotService.getIOTComponent().startsWith("[")); + } + } +} diff --git a/src/test/java/com/iemr/admin/utils/CookieUtilTest.java b/src/test/java/com/iemr/admin/utils/CookieUtilTest.java new file mode 100644 index 0000000..3e9fbda --- /dev/null +++ b/src/test/java/com/iemr/admin/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.admin.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/admin/utils/FilterConfigTest.java b/src/test/java/com/iemr/admin/utils/FilterConfigTest.java new file mode 100644 index 0000000..46580be --- /dev/null +++ b/src/test/java/com/iemr/admin/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.admin.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/admin/utils/JwtAuthenticationUtilTest.java b/src/test/java/com/iemr/admin/utils/JwtAuthenticationUtilTest.java new file mode 100644 index 0000000..8c2f0c7 --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/JwtAuthenticationUtilTest.java @@ -0,0 +1,221 @@ +/* +* 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.admin.utils; + +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.admin.data.user.M_User; +import com.iemr.admin.repository.user.UserLoginRepo; +import com.iemr.admin.utils.exception.IEMRException; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; + +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.anyInt; +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; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("JwtAuthenticationUtil Test Suite") +class JwtAuthenticationUtilTest { + + private static final String TOKEN = "a.jwt.token"; + private static final String USER_ID = "3117"; + + @Mock + private CookieUtil cookieUtil; + + @Mock + private JwtUtil jwtUtil; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + @Mock + private UserLoginRepo userLoginRepo; + + @Mock + private Claims claims; + + @Mock + private HttpServletRequest request; + + private JwtAuthenticationUtil authenticationUtil; + + @BeforeEach + void setUp() { + authenticationUtil = new JwtAuthenticationUtil(cookieUtil, jwtUtil); + ReflectionTestUtils.setField(authenticationUtil, "redisTemplate", redisTemplate); + ReflectionTestUtils.setField(authenticationUtil, "userLoginRepo", userLoginRepo); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + } + + @Test + @DisplayName("validateJwtToken should answer the subject when the cookie carries a valid token") + void validateJwtToken_shouldAnswerSubjectForValidToken() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.of(TOKEN)); + when(jwtUtil.validateToken(TOKEN)).thenReturn(claims); + when(claims.getSubject()).thenReturn("dr.mehta"); + + ResponseEntity response = authenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("dr.mehta", response.getBody()); + } + + @Test + @DisplayName("validateJwtToken should refuse a request that carries no token cookie") + void validateJwtToken_shouldRefuseWhenCookieAbsent() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.empty()); + + ResponseEntity response = authenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertEquals("Error 401: Unauthorized - JWT Token is not set!", response.getBody()); + } + + @Test + @DisplayName("validateJwtToken should refuse a token the validator rejects") + void validateJwtToken_shouldRefuseInvalidToken() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.of(TOKEN)); + when(jwtUtil.validateToken(TOKEN)).thenReturn(null); + + ResponseEntity response = authenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertEquals("Error 401: Unauthorized - Invalid JWT Token!", response.getBody()); + } + + @Test + @DisplayName("validateJwtToken should refuse a valid token that names no subject") + void validateJwtToken_shouldRefuseTokenWithoutSubject() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.of(TOKEN)); + when(jwtUtil.validateToken(TOKEN)).thenReturn(claims); + when(claims.getSubject()).thenReturn(null); + + ResponseEntity response = authenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertEquals("Error 401: Unauthorized - Username is missing!", response.getBody()); + } + + @Test + @DisplayName("validateJwtToken should refuse a valid token whose subject is blank") + void validateJwtToken_shouldRefuseTokenWithBlankSubject() { + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(Optional.of(TOKEN)); + when(jwtUtil.validateToken(TOKEN)).thenReturn(claims); + when(claims.getSubject()).thenReturn(""); + + ResponseEntity response = authenticationUtil.validateJwtToken(request); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertEquals("Error 401: Unauthorized - Username is missing!", response.getBody()); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should accept a user the cache already holds") + void validateUserIdAndJwtToken_shouldAcceptCachedUser() throws IEMRException { + M_User cached = new M_User(); + cached.setUserID(3117); + when(jwtUtil.validateToken(TOKEN)).thenReturn(claims); + when(claims.get("userId", String.class)).thenReturn(USER_ID); + when(valueOperations.get("user_" + USER_ID)).thenReturn(cached); + + assertTrue(authenticationUtil.validateUserIdAndJwtToken(TOKEN)); + verify(userLoginRepo, never()).findByUserID(anyInt()); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should fall back to the database and cache what it finds") + void validateUserIdAndJwtToken_shouldFallBackToDatabaseAndCache() throws IEMRException { + M_User stored = new M_User(); + stored.setUserID(3117); + stored.setUserName("dr.mehta"); + when(jwtUtil.validateToken(TOKEN)).thenReturn(claims); + when(claims.get("userId", String.class)).thenReturn(USER_ID); + when(valueOperations.get("user_" + USER_ID)).thenReturn(null); + when(userLoginRepo.findByUserID(3117)).thenReturn(stored); + + assertTrue(authenticationUtil.validateUserIdAndJwtToken(TOKEN)); + verify(valueOperations).set(eq("user_" + USER_ID), any(M_User.class), anyLong(), eq(TimeUnit.MINUTES)); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should reject a token the validator does not accept") + void validateUserIdAndJwtToken_shouldRejectInvalidToken() { + when(jwtUtil.validateToken(TOKEN)).thenReturn(null); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> authenticationUtil.validateUserIdAndJwtToken(TOKEN)); + assertTrue(thrown.getMessage().contains("Invalid JWT token."), thrown.getMessage()); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should reject a user neither the cache nor the database knows") + void validateUserIdAndJwtToken_shouldRejectUnknownUser() { + when(jwtUtil.validateToken(TOKEN)).thenReturn(claims); + when(claims.get("userId", String.class)).thenReturn(USER_ID); + when(valueOperations.get("user_" + USER_ID)).thenReturn(null); + when(userLoginRepo.findByUserID(3117)).thenReturn(null); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> authenticationUtil.validateUserIdAndJwtToken(TOKEN)); + assertTrue(thrown.getMessage().contains("Invalid User ID."), thrown.getMessage()); + } + + @Test + @DisplayName("validateUserIdAndJwtToken should surface a non-numeric user id as a validation failure") + void validateUserIdAndJwtToken_shouldRejectNonNumericUserId() { + when(jwtUtil.validateToken(TOKEN)).thenReturn(claims); + when(claims.get("userId", String.class)).thenReturn("not-a-number"); + when(valueOperations.get(anyString())).thenReturn(null); + + assertThrows(IEMRException.class, () -> authenticationUtil.validateUserIdAndJwtToken(TOKEN)); + } +} diff --git a/src/test/java/com/iemr/admin/utils/JwtUserIdValidationFilterTest.java b/src/test/java/com/iemr/admin/utils/JwtUserIdValidationFilterTest.java new file mode 100644 index 0000000..f6494ef --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/JwtUserIdValidationFilterTest.java @@ -0,0 +1,415 @@ +/* +* 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.admin.utils; + +import java.util.Arrays; + +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.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import com.iemr.admin.utils.exception.IEMRException; +import com.iemr.admin.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 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.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * The filter is the service's front door: it lets the monitoring and login + * endpoints through untouched, turns away calls from origins that are not on the + * allow list, and rejects anything else that carries no usable JWT. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("JwtUserIdValidationFilter Test Suite") +class JwtUserIdValidationFilterTest { + + private static final String ALLOWED_ORIGINS = "https://amrit.example.org,http://localhost:*"; + private static final String TOKEN = "a.jwt.token"; + + @Mock + private JwtAuthenticationUtil jwtAuthenticationUtil; + + private RecordingChain chain; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private JwtUserIdValidationFilter filter; + + /** Remembers what the filter handed on, so the wrapping can be inspected. */ + private static final class RecordingChain implements FilterChain { + private ServletRequest passedRequest; + private int invocations; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + this.passedRequest = servletRequest; + this.invocations++; + } + } + + @BeforeEach + void setUp() { + chain = new RecordingChain(); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + request.setMethod("POST"); + filter = new JwtUserIdValidationFilter(jwtAuthenticationUtil, ALLOWED_ORIGINS); + } + + @Nested + @DisplayName("Monitoring endpoints") + class MonitoringEndpointTests { + + @Test + @DisplayName("doFilter should let the health endpoint through without looking at a token") + void doFilter_shouldPassHealthEndpointThrough() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/health"); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + assertSameRequestPassedOn(); + verifyNoInteractions(jwtAuthenticationUtil); + } + + @Test + @DisplayName("doFilter should let the version endpoint through without looking at a token") + void doFilter_shouldPassVersionEndpointThrough() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/version"); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + verifyNoInteractions(jwtAuthenticationUtil); + } + + @Test + @DisplayName("doFilter should let the monitoring endpoints through even from an unlisted origin") + void doFilter_shouldPassHealthEndpointThroughForAnyOrigin() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/health"); + request.addHeader("Origin", "https://attacker.example.net"); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + } + } + + @Nested + @DisplayName("Origin validation") + class OriginValidationTests { + + @Test + @DisplayName("doFilter should turn away a request from an origin that is not on the allow list") + void doFilter_shouldTurnAwayUnlistedOrigin() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Origin", "https://attacker.example.net"); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + assertEquals(0, chain.invocations); + } + + @Test + @DisplayName("doFilter should carry on for a request from an origin on the allow list") + void doFilter_shouldCarryOnForAllowedOrigin() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Origin", "https://amrit.example.org"); + request.setCookies(new Cookie(Constants.JWT_TOKEN, TOKEN)); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken(TOKEN)).thenReturn(true); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + } + + @Test + @DisplayName("doFilter should accept a wildcard localhost origin from the allow list") + void doFilter_shouldAcceptWildcardLocalhostOrigin() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Origin", "http://localhost:4200"); + request.setCookies(new Cookie(Constants.JWT_TOKEN, TOKEN)); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken(TOKEN)).thenReturn(true); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + } + + @Test + @DisplayName("doFilter should turn away every origin when no allow list is configured") + void doFilter_shouldTurnAwayEveryOriginWithoutAnAllowList() throws Exception { + filter = new JwtUserIdValidationFilter(jwtAuthenticationUtil, " "); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Origin", "https://amrit.example.org"); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + @Test + @DisplayName("doFilter should turn away a preflight that names no origin") + void doFilter_shouldTurnAwayPreflightWithoutOrigin() throws Exception { + request.setMethod("OPTIONS"); + request.setRequestURI("/zonemaster/get/zones"); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + assertEquals(0, chain.invocations); + } + + @Test + @DisplayName("doFilter should turn away a preflight from an unlisted origin") + void doFilter_shouldTurnAwayPreflightFromUnlistedOrigin() throws Exception { + request.setMethod("OPTIONS"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Origin", "https://attacker.example.net"); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + } + + @Nested + @DisplayName("Public endpoints") + class PublicEndpointTests { + + @Test + @DisplayName("doFilter should let the login endpoint through untouched") + void doFilter_shouldPassLoginThrough() throws Exception { + request.setRequestURI("/user/userAuthenticate"); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + assertSameRequestPassedOn(); + verifyNoInteractions(jwtAuthenticationUtil); + } + + @Test + @DisplayName("doFilter should let the concurrent-session logout through untouched") + void doFilter_shouldPassConcurrentLogoutThrough() throws Exception { + request.setRequestURI("/user/logOutUserFromConcurrentSession"); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + } + + @Test + @DisplayName("doFilter should let the swagger UI and its api-docs through untouched") + void doFilter_shouldPassSwaggerThrough() throws Exception { + for (String uri : Arrays.asList("/swagger-ui/index.html", "/v3/api-docs", "/public/anything", + "/user/refreshToken")) { + RecordingChain publicChain = new RecordingChain(); + MockHttpServletRequest publicRequest = new MockHttpServletRequest(); + publicRequest.setMethod("GET"); + publicRequest.setRequestURI(uri); + + filter.doFilter(publicRequest, new MockHttpServletResponse(), publicChain); + + assertEquals(1, publicChain.invocations, uri + " must be let through untouched"); + } + verifyNoInteractions(jwtAuthenticationUtil); + } + } + + @Nested + @DisplayName("Token validation") + class TokenValidationTests { + + @Test + @DisplayName("doFilter should carry on with a blanked Authorization header for a valid cookie token") + void doFilter_shouldCarryOnForValidCookieToken() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.setCookies(new Cookie(Constants.JWT_TOKEN, TOKEN)); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken(TOKEN)).thenReturn(true); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + assertTrue(chain.passedRequest instanceof AuthorizationHeaderRequestWrapper, + "a validated request must reach the controllers with its Authorization header replaced"); + } + + @Test + @DisplayName("doFilter should carry on for a valid token supplied in the Jwttoken header") + void doFilter_shouldCarryOnForValidHeaderToken() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader(Constants.JWT_TOKEN, TOKEN); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken(TOKEN)).thenReturn(true); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + assertTrue(chain.passedRequest instanceof AuthorizationHeaderRequestWrapper); + } + + @Test + @DisplayName("doFilter should reject a cookie token the validator does not accept") + void doFilter_shouldRejectUnacceptedCookieToken() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.setCookies(new Cookie(Constants.JWT_TOKEN, TOKEN)); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken(TOKEN)).thenReturn(false); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + assertEquals(0, chain.invocations); + } + + @Test + @DisplayName("doFilter should reject a request that carries no token at all") + void doFilter_shouldRejectRequestWithoutToken() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + verify(jwtAuthenticationUtil, never()).validateUserIdAndJwtToken(anyString()); + } + + @Test + @DisplayName("doFilter should reject rather than propagate a failure raised while validating") + void doFilter_shouldRejectWhenValidationRaises() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.setCookies(new Cookie(Constants.JWT_TOKEN, TOKEN)); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken(TOKEN)) + .thenThrow(new IEMRException("Validation error: Invalid User ID.")); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + assertEquals(0, chain.invocations); + } + } + + @Nested + @DisplayName("Mobile clients") + class MobileClientTests { + + @Test + @DisplayName("doFilter should carry a mobile call through on its Authorization header alone") + void doFilter_shouldCarryMobileCallThrough() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader(Constants.USER_AGENT, "okhttp/4.9.3"); + request.addHeader("Authorization", "Bearer session-key"); + + filter.doFilter(request, response, chain); + + assertEquals(1, chain.invocations); + assertSameRequestPassedOn(); + assertNull(UserAgentContext.getUserAgent(), "the user agent must not outlive the request"); + } + + @Test + @DisplayName("doFilter should reject a mobile call that carries no Authorization header") + void doFilter_shouldRejectMobileCallWithoutAuthorization() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader(Constants.USER_AGENT, "okhttp/4.9.3"); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + } + + @Test + @DisplayName("doFilter should reject a browser call that carries only an Authorization header") + void doFilter_shouldRejectBrowserCallWithOnlyAuthorization() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader(Constants.USER_AGENT, "Mozilla/5.0"); + request.addHeader("Authorization", "Bearer session-key"); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + } + } + + @Nested + @DisplayName("userId cookie") + class UserIdCookieTests { + + @Test + @DisplayName("doFilter should expire a userId cookie a caller tries to smuggle in") + void doFilter_shouldExpireUserIdCookie() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.setCookies(new Cookie("userId", "3117"), new Cookie(Constants.JWT_TOKEN, TOKEN)); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken(TOKEN)).thenReturn(true); + + filter.doFilter(request, response, chain); + + Cookie cleared = response.getCookie("userId"); + assertNotNull(cleared, "the smuggled cookie must be sent back expired"); + assertEquals(0, cleared.getMaxAge()); + assertNull(cleared.getValue()); + assertTrue(cleared.isHttpOnly()); + assertTrue(cleared.getSecure()); + } + + @Test + @DisplayName("doFilter should leave the response cookies alone when the request carries none") + void doFilter_shouldLeaveCookiesAloneWhenNoneArePresent() throws Exception { + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader(Constants.JWT_TOKEN, TOKEN); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken(TOKEN)).thenReturn(true); + + filter.doFilter(request, response, chain); + + assertNull(response.getCookie("userId")); + } + } + + private void assertSameRequestPassedOn() { + assertTrue(chain.passedRequest == request, "the original request must be handed on unwrapped"); + } +} diff --git a/src/test/java/com/iemr/admin/utils/JwtUtilTest.java b/src/test/java/com/iemr/admin/utils/JwtUtilTest.java new file mode 100644 index 0000000..145153a --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/JwtUtilTest.java @@ -0,0 +1,198 @@ +/* +* 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.admin.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.ArgumentCaptor; +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.junit.jupiter.api.Assertions.assertTrue; +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.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("JwtUtil Test Suite") +class JwtUtilTest { + + private static final String SECRET = "amrit-tm-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/admin/utils/RestTemplateUtilTest.java b/src/test/java/com/iemr/admin/utils/RestTemplateUtilTest.java new file mode 100644 index 0000000..334922a --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/RestTemplateUtilTest.java @@ -0,0 +1,219 @@ +/* +* 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.admin.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; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@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(Constants.JWT_TOKEN, "header-token"); + + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertEquals("header-token", entity.getHeaders().getFirst(Constants.JWT_TOKEN)); + } + + @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(Constants.JWT_TOKEN, "header-token"); + request.setCookies(new Cookie("Jwttoken", "cookie-token")); + + HttpEntity entity = RestTemplateUtil.createRequestEntity(BODY, AUTHORIZATION); + + assertEquals("header-token", entity.getHeaders().getFirst(Constants.JWT_TOKEN)); + assertEquals("Jwttoken=cookie-token", entity.getHeaders().getFirst(HttpHeaders.COOKIE)); + } + } + + @Nested + @DisplayName("getJwttokenFromHeaders") + class GetJwttokenFromHeadersTests { + + @Test + @DisplayName("should replay the caller's token cookie onto the outbound headers") + void getJwttokenFromHeaders_shouldReplayTokenCookie() { + MockHttpServletRequest inbound = new MockHttpServletRequest(); + inbound.setCookies(new Cookie(Constants.JWT_TOKEN, "cookie-token")); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(inbound)); + HttpHeaders headers = new HttpHeaders(); + + RestTemplateUtil.getJwttokenFromHeaders(headers); + + assertEquals("Jwttoken=cookie-token", headers.getFirst(HttpHeaders.COOKIE)); + } + + @Test + @DisplayName("should fall back to the caller's token header when no cookie is present") + void getJwttokenFromHeaders_shouldFallBackToTokenHeader() { + MockHttpServletRequest inbound = new MockHttpServletRequest(); + inbound.addHeader(Constants.JWT_TOKEN, "header-token"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(inbound)); + HttpHeaders headers = new HttpHeaders(); + + RestTemplateUtil.getJwttokenFromHeaders(headers); + + assertEquals("header-token", headers.getFirst(Constants.JWT_TOKEN)); + assertNull(headers.getFirst(HttpHeaders.COOKIE)); + } + + @Test + @DisplayName("should carry the mobile user agent when one is in scope") + void getJwttokenFromHeaders_shouldCarryUserAgent() { + MockHttpServletRequest inbound = new MockHttpServletRequest(); + inbound.setCookies(new Cookie(Constants.JWT_TOKEN, "cookie-token")); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(inbound)); + UserAgentContext.setUserAgent("okhttp/4.9.3"); + HttpHeaders headers = new HttpHeaders(); + try { + RestTemplateUtil.getJwttokenFromHeaders(headers); + } finally { + UserAgentContext.clear(); + } + + assertEquals("okhttp/4.9.3", headers.getFirst(HttpHeaders.USER_AGENT)); + } + + @Test + @DisplayName("should leave the headers untouched when no request is in scope") + void getJwttokenFromHeaders_shouldLeaveHeadersUntouchedWithoutRequest() { + RequestContextHolder.resetRequestAttributes(); + HttpHeaders headers = new HttpHeaders(); + + RestTemplateUtil.getJwttokenFromHeaders(headers); + + assertTrue(headers.isEmpty()); + } + } +} diff --git a/src/test/java/com/iemr/admin/utils/TokenDenylistTest.java b/src/test/java/com/iemr/admin/utils/TokenDenylistTest.java new file mode 100644 index 0000000..f7366ac --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/TokenDenylistTest.java @@ -0,0 +1,175 @@ +/* +* 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.admin.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.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.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.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; + +/** + * The denylist keeps the identifiers of tokens that have been signed out, so a + * stolen but otherwise valid token stops being accepted. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("TokenDenylist Test Suite") +class TokenDenylistTest { + + private static final String JTI = "b0f1c2d3-4e5f-6789-abcd-ef0123456789"; + private static final String KEY = "denied_" + JTI; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + private TokenDenylist tokenDenylist; + + @BeforeEach + void setUp() { + tokenDenylist = new TokenDenylist(); + ReflectionTestUtils.setField(tokenDenylist, "redisTemplate", redisTemplate); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + } + + @Nested + @DisplayName("addTokenToDenylist") + class AddTokenTests { + + @Test + @DisplayName("should store the prefixed key for the lifetime the caller asks for") + void addTokenToDenylist_shouldStorePrefixedKeyWithExpiry() { + tokenDenylist.addTokenToDenylist(JTI, 600_000L); + + verify(valueOperations).set(KEY, " ", 600_000L, TimeUnit.MILLISECONDS); + } + + @Test + @DisplayName("should ignore a null token id rather than write an orphan key") + void addTokenToDenylist_shouldIgnoreNullId() { + tokenDenylist.addTokenToDenylist(null, 600_000L); + + verifyNoInteractions(valueOperations); + } + + @Test + @DisplayName("should ignore a blank token id") + void addTokenToDenylist_shouldIgnoreBlankId() { + tokenDenylist.addTokenToDenylist(" ", 600_000L); + + verifyNoInteractions(valueOperations); + } + + @Test + @DisplayName("should refuse a null expiry rather than denylist a token forever") + void addTokenToDenylist_shouldRefuseNullExpiry() { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> tokenDenylist.addTokenToDenylist(JTI, null)); + + assertEquals("Expiration time must be positive", thrown.getMessage()); + verify(valueOperations, never()).set(anyString(), anyString()); + } + + @Test + @DisplayName("should refuse an expiry that has already passed") + void addTokenToDenylist_shouldRefuseNonPositiveExpiry() { + assertThrows(IllegalArgumentException.class, () -> tokenDenylist.addTokenToDenylist(JTI, 0L)); + assertThrows(IllegalArgumentException.class, () -> tokenDenylist.addTokenToDenylist(JTI, -1L)); + } + + @Test + @DisplayName("should surface a store failure rather than report a sign-out that did not happen") + void addTokenToDenylist_shouldSurfaceStoreFailure() { + doThrow(new IllegalStateException("redis is down")) + .when(valueOperations).set(KEY, " ", 600_000L, TimeUnit.MILLISECONDS); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> tokenDenylist.addTokenToDenylist(JTI, 600_000L)); + + assertEquals("Failed to denylist token", thrown.getMessage()); + } + } + + @Nested + @DisplayName("isTokenDenylisted") + class IsTokenDenylistedTests { + + @Test + @DisplayName("should report a token whose key the store still holds") + void isTokenDenylisted_shouldReportDenylistedToken() { + when(redisTemplate.hasKey(KEY)).thenReturn(Boolean.TRUE); + + assertTrue(tokenDenylist.isTokenDenylisted(JTI)); + } + + @Test + @DisplayName("should clear a token whose key the store no longer holds") + void isTokenDenylisted_shouldClearUnknownToken() { + when(redisTemplate.hasKey(KEY)).thenReturn(Boolean.FALSE); + + assertFalse(tokenDenylist.isTokenDenylisted(JTI)); + } + + @Test + @DisplayName("should treat a null token id as not denylisted") + void isTokenDenylisted_shouldTreatNullIdAsAllowed() { + assertFalse(tokenDenylist.isTokenDenylisted(null)); + } + + @Test + @DisplayName("should treat a blank token id as not denylisted") + void isTokenDenylisted_shouldTreatBlankIdAsAllowed() { + assertFalse(tokenDenylist.isTokenDenylisted(" ")); + } + + @Test + @DisplayName("should let requests through rather than block everyone when the store fails") + void isTokenDenylisted_shouldAllowWhenStoreFails() { + when(redisTemplate.hasKey(KEY)).thenThrow(new IllegalStateException("redis is down")); + + assertFalse(tokenDenylist.isTokenDenylisted(JTI)); + } + } +} diff --git a/src/test/java/com/iemr/admin/utils/UserAgentContextTest.java b/src/test/java/com/iemr/admin/utils/UserAgentContextTest.java new file mode 100644 index 0000000..de95140 --- /dev/null +++ b/src/test/java/com/iemr/admin/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.admin.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/admin/utils/config/ConfigPropertiesTest.java b/src/test/java/com/iemr/admin/utils/config/ConfigPropertiesTest.java new file mode 100644 index 0000000..512fcf0 --- /dev/null +++ b/src/test/java/com/iemr/admin/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.admin.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.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.redis.port")); + } + + @Test + @DisplayName("getInteger should parse an integer property") + void getInteger_shouldParseIntegerProperty() { + assertEquals(7200, 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(7200L, 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, ConfigProperties.getLong("spring.session.store-type")); + } + + @Test + @DisplayName("getFloat should parse a numeric property") + void getFloat_shouldParseNumericProperty() { + assertEquals(6379F, ConfigProperties.getFloat("spring.redis.port")); + } + + @Test + @DisplayName("getFloat should fall back to zero when the value is not a number") + void getFloat_shouldFallBackToZeroForNonNumericValue() { + assertEquals(0F, 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(7200, ConfigProperties.getSessionExpiryTime()); + } + + @Test + @DisplayName("getRedisPort should fall back to zero when no iemr.redis.port is configured") + void getRedisPort_shouldFallBackToZeroWhenUnconfigured() { + assertEquals(0, ConfigProperties.getRedisPort()); + } + + @Test + @DisplayName("getRedisUrl should return null when no iemr.redis.url is configured") + void getRedisUrl_shouldReturnNullWhenUnconfigured() { + assertNull(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", 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", ConfigProperties.getPassword("db.password")); + } + } +} diff --git a/src/test/java/com/iemr/admin/utils/gateway/email/GenericEmailServiceImplTest.java b/src/test/java/com/iemr/admin/utils/gateway/email/GenericEmailServiceImplTest.java new file mode 100644 index 0000000..5a9b243 --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/gateway/email/GenericEmailServiceImplTest.java @@ -0,0 +1,156 @@ +/* +* 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.admin.utils.gateway.email; + +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.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mail.MailSendException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; + +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.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +@DisplayName("GenericEmailServiceImpl Test Suite") +class GenericEmailServiceImplTest { + + @Mock + private JavaMailSender javaMailSender; + + private GenericEmailServiceImpl emailService; + + @BeforeEach + @DisplayName("Wire the service with a mocked mail sender before each test") + void setUp() { + emailService = new GenericEmailServiceImpl(); + emailService.setJavaMailSender(javaMailSender); + } + + private String request(String to) { + return String.format( + "{\"to\":\"%s\",\"from\":\"no-reply@amrit.example.org\"," + + "\"subject\":\"Specialist availability alert\"," + + "\"message\":\"Specialist availability was updated.\"}", + to); + } + + private SimpleMailMessage captureSentMessage() { + ArgumentCaptor captor = ArgumentCaptor.forClass(SimpleMailMessage.class); + verify(javaMailSender).send(captor.capture()); + return captor.getValue(); + } + + @Nested + @DisplayName("sendEmail without a template") + class SendEmailTests { + + @Test + @DisplayName("sendEmail should populate every field of the message from the JSON request") + void sendEmail_shouldPopulateMessageFromJsonRequest() throws Exception { + emailService.sendEmail(request("ops@amrit.example.org")); + + SimpleMailMessage sent = captureSentMessage(); + assertArrayEquals(new String[] { "ops@amrit.example.org" }, sent.getTo()); + assertEquals("no-reply@amrit.example.org", sent.getFrom()); + assertEquals("Specialist availability alert", sent.getSubject()); + assertEquals("Specialist availability was updated.", sent.getText()); + } + + @Test + @DisplayName("sendEmail should split a semicolon-separated recipient list into multiple addresses") + void sendEmail_shouldSplitSemicolonSeparatedRecipients() throws Exception { + emailService.sendEmail(request("ops@amrit.example.org;admin@amrit.example.org")); + + assertArrayEquals(new String[] { "ops@amrit.example.org", "admin@amrit.example.org" }, + captureSentMessage().getTo()); + } + + @Test + @DisplayName("sendEmail should reject a request that is missing a mandatory field") + void sendEmail_shouldRejectRequestMissingMandatoryField() throws Exception { + String incomplete = "{\"to\":\"ops@amrit.example.org\"}"; + + assertThrows(JSONException.class, () -> emailService.sendEmail(incomplete)); + verify(javaMailSender, never()).send(org.mockito.ArgumentMatchers.any(SimpleMailMessage.class)); + } + + @Test + @DisplayName("sendEmail should propagate a mail transport failure") + void sendEmail_shouldPropagateTransportFailure() throws Exception { + doThrow(new MailSendException("smtp unreachable")) + .when(javaMailSender).send(org.mockito.ArgumentMatchers.any(SimpleMailMessage.class)); + + assertThrows(MailSendException.class, () -> emailService.sendEmail(request("ops@amrit.example.org"))); + } + } + + @Nested + @DisplayName("sendEmail with a template") + class SendEmailWithTemplateTests { + + @Test + @DisplayName("sendEmail with a template should populate the message from the JSON request") + void sendEmail_withTemplate_shouldPopulateMessageFromJsonRequest() throws Exception { + emailService.sendEmail(request("ops@amrit.example.org"), "availability-alert-template"); + + SimpleMailMessage sent = captureSentMessage(); + assertArrayEquals(new String[] { "ops@amrit.example.org" }, sent.getTo()); + assertEquals("Specialist availability alert", sent.getSubject()); + assertEquals("Specialist availability was updated.", sent.getText()); + } + + @Test + @DisplayName("sendEmail with a template should keep a semicolon list as a single recipient") + void sendEmail_withTemplate_shouldKeepRecipientListUnsplit() throws Exception { + emailService.sendEmail(request("ops@amrit.example.org;admin@amrit.example.org"), + "availability-alert-template"); + + assertArrayEquals(new String[] { "ops@amrit.example.org;admin@amrit.example.org" }, + captureSentMessage().getTo()); + } + } + + @Nested + @DisplayName("sendEmailWithAttachment") + class SendEmailWithAttachmentTests { + + @Test + @DisplayName("sendEmailWithAttachment is not implemented and should send nothing") + void sendEmailWithAttachment_shouldSendNothing() throws Exception { + emailService.sendEmailWithAttachment(request("ops@amrit.example.org"), "template"); + + verify(javaMailSender, never()).send(org.mockito.ArgumentMatchers.any(SimpleMailMessage.class)); + } + } +} diff --git a/src/test/java/com/iemr/admin/utils/http/AuthorizationHeaderRequestWrapperTest.java b/src/test/java/com/iemr/admin/utils/http/AuthorizationHeaderRequestWrapperTest.java new file mode 100644 index 0000000..e20b7fd --- /dev/null +++ b/src/test/java/com/iemr/admin/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.admin.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/admin/utils/http/HTTPRequestInterceptorTest.java b/src/test/java/com/iemr/admin/utils/http/HTTPRequestInterceptorTest.java new file mode 100644 index 0000000..0d00b90 --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/http/HTTPRequestInterceptorTest.java @@ -0,0 +1,312 @@ +/* +* 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.admin.utils.http; + +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.InjectMocks; +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.MockHttpServletRequest; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.servlet.ModelAndView; + +import com.iemr.admin.utils.redis.RedisSessionException; +import com.iemr.admin.utils.redis.RedisStorage; +import com.iemr.admin.utils.sessionobject.SessionObject; + +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.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@DisplayName("HTTPRequestInterceptor Test Suite") +class HTTPRequestInterceptorTest { + + private static final String SESSION_KEY = "0d5f3a7c-2b91-4e6d-8f10-3a4b5c6d7e8f"; + private static final String SESSION_PAYLOAD = "{\"userName\":\"amrit-admin\"}"; + + @Mock + private RedisStorage redisStorage; + + @Mock + private SessionObject sessionObject; + + @InjectMocks + private HTTPRequestInterceptor interceptor; + + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @BeforeEach + @DisplayName("Prepare a request and response before each test") + void setUp() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + } + + @Nested + @DisplayName("preHandle") + class PreHandleTests { + + @Test + @DisplayName("preHandle should admit a request whose session key resolves in Redis") + void preHandle_shouldAdmitRequestWithResolvableSessionKey() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", SESSION_KEY); + when(sessionObject.getSessionObject(SESSION_KEY)).thenReturn(SESSION_PAYLOAD); + + assertTrue(interceptor.preHandle(request, response, new Object())); + } + + @Test + @DisplayName("preHandle should strip the Bearer prefix before resolving the session key") + void preHandle_shouldStripBearerPrefix() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", "Bearer " + SESSION_KEY); + when(sessionObject.getSessionObject(SESSION_KEY)).thenReturn(SESSION_PAYLOAD); + + assertTrue(interceptor.preHandle(request, response, new Object())); + verify(sessionObject).getSessionObject(SESSION_KEY); + } + + @Test + @DisplayName("preHandle should reject a request whose session key is unknown to Redis") + void preHandle_shouldRejectUnknownSessionKey() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", SESSION_KEY); + when(sessionObject.getSessionObject(SESSION_KEY)).thenReturn(null); + + assertFalse(interceptor.preHandle(request, response, new Object())); + assertTrue(response.getContentAsString().contains("5000"), response.getContentAsString()); + } + + @Test + @DisplayName("preHandle should reject a request whose session lookup fails") + void preHandle_shouldRejectWhenSessionLookupFails() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", SESSION_KEY); + when(sessionObject.getSessionObject(SESSION_KEY)) + .thenThrow(new RedisSessionException("Unable to fetch session object from Redis server")); + + assertFalse(interceptor.preHandle(request, response, new Object())); + assertNull(response.getHeader("Access-Control-Allow-Origin"), + "no allow-list is configured, so no origin may be echoed back"); + } + + @Test + @DisplayName("preHandle should echo an allowed origin on the rejection so the browser can read it") + void preHandle_shouldEchoAllowedOriginOnRejection() throws Exception { + ReflectionTestUtils.setField(interceptor, "allowedOrigins", "https://amrit.example.org"); + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", SESSION_KEY); + request.addHeader("Origin", "https://amrit.example.org"); + when(sessionObject.getSessionObject(SESSION_KEY)).thenReturn(null); + + assertFalse(interceptor.preHandle(request, response, new Object())); + assertEquals("https://amrit.example.org", response.getHeader("Access-Control-Allow-Origin")); + assertEquals("true", response.getHeader("Access-Control-Allow-Credentials")); + } + + @Test + @DisplayName("preHandle should withhold the CORS headers from a rejection for an unlisted origin") + void preHandle_shouldWithholdCorsHeadersForUnlistedOrigin() throws Exception { + ReflectionTestUtils.setField(interceptor, "allowedOrigins", "https://amrit.example.org"); + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", SESSION_KEY); + request.addHeader("Origin", "https://attacker.example.net"); + when(sessionObject.getSessionObject(SESSION_KEY)).thenReturn(null); + + assertFalse(interceptor.preHandle(request, response, new Object())); + assertNull(response.getHeader("Access-Control-Allow-Origin")); + } + + @Test + @DisplayName("preHandle should match a wildcard localhost entry in the allow list") + void preHandle_shouldMatchWildcardLocalhostOrigin() throws Exception { + ReflectionTestUtils.setField(interceptor, "allowedOrigins", "http://localhost:*"); + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", SESSION_KEY); + request.addHeader("Origin", "http://localhost:4200"); + when(sessionObject.getSessionObject(SESSION_KEY)).thenReturn(null); + + assertFalse(interceptor.preHandle(request, response, new Object())); + assertEquals("http://localhost:4200", response.getHeader("Access-Control-Allow-Origin")); + } + + @Test + @DisplayName("preHandle should pass a request without an Authorization header on to the security filters") + void preHandle_shouldPassRequestWithoutAuthorizationOn() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + + assertTrue(interceptor.preHandle(request, response, new Object())); + verify(sessionObject, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should pass a request with a blank Authorization header on unchecked") + void preHandle_shouldPassBlankAuthorizationOn() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", ""); + + assertTrue(interceptor.preHandle(request, response, new Object())); + verify(sessionObject, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should admit the unauthenticated health and version endpoints") + void preHandle_shouldAdmitPublicEndpoints() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/health"); + assertTrue(interceptor.preHandle(request, response, new Object())); + + MockHttpServletRequest versionRequest = new MockHttpServletRequest(); + versionRequest.setMethod("GET"); + versionRequest.setRequestURI("/version"); + assertTrue(interceptor.preHandle(versionRequest, new MockHttpServletResponse(), new Object())); + + verify(sessionObject, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should admit the swagger UI without checking a session") + void preHandle_shouldAdmitSwaggerUi() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/swagger-ui/index.html"); + + assertTrue(interceptor.preHandle(request, response, new Object())); + verify(sessionObject, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should admit the swagger support resources on a session key") + void preHandle_shouldAdmitSwaggerSupportResources() throws Exception { + for (String uri : new String[] { "/v3/api-docs", "/swagger-resources", "/swagger-config", + "/ui", "/index.html", "/swagger-initializer.js" }) { + MockHttpServletRequest swaggerRequest = new MockHttpServletRequest(); + swaggerRequest.setMethod("GET"); + swaggerRequest.setRequestURI(uri); + swaggerRequest.addHeader("Authorization", SESSION_KEY); + + assertTrue(interceptor.preHandle(swaggerRequest, new MockHttpServletResponse(), new Object()), + uri + " must be admitted without a session lookup"); + } + verify(sessionObject, never()).getSessionObject(anyString()); + } + + @Test + @DisplayName("preHandle should reject the error endpoint") + void preHandle_shouldRejectTheErrorEndpoint() throws Exception { + request.setMethod("GET"); + request.setRequestURI("/error"); + request.addHeader("Authorization", SESSION_KEY); + + assertFalse(interceptor.preHandle(request, response, new Object())); + } + + @Test + @DisplayName("preHandle should admit an OPTIONS preflight without checking a session") + void preHandle_shouldAdmitOptionsPreflight() throws Exception { + request.setMethod("OPTIONS"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", SESSION_KEY); + + assertTrue(interceptor.preHandle(request, response, new Object())); + verify(sessionObject, never()).getSessionObject(anyString()); + } + } + + @Nested + @DisplayName("postHandle") + class PostHandleTests { + + @Test + @DisplayName("postHandle should refresh the session it resolved for the request") + void postHandle_shouldRefreshTheSession() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", "Bearer " + SESSION_KEY); + when(sessionObject.getSessionObject(SESSION_KEY)).thenReturn(SESSION_PAYLOAD); + + interceptor.postHandle(request, response, new Object(), new ModelAndView()); + + verify(sessionObject).updateSessionObject(SESSION_KEY, SESSION_PAYLOAD); + } + + @Test + @DisplayName("postHandle should do nothing when the request carried no Authorization header") + void postHandle_shouldDoNothingWithoutAuthorization() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + + interceptor.postHandle(request, response, new Object(), new ModelAndView()); + + verify(sessionObject, never()).updateSessionObject(anyString(), anyString()); + } + + @Test + @DisplayName("postHandle should swallow a session refresh failure") + void postHandle_shouldSwallowRefreshFailure() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + request.addHeader("Authorization", SESSION_KEY); + when(sessionObject.getSessionObject(SESSION_KEY)) + .thenThrow(new RedisSessionException("Unable to fetch session object from Redis server")); + + interceptor.postHandle(request, response, new Object(), new ModelAndView()); + + verify(sessionObject, never()).updateSessionObject(anyString(), anyString()); + } + } + + @Test + @DisplayName("afterCompletion should complete without touching the session store") + void afterCompletion_shouldCompleteWithoutTouchingTheSessionStore() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/zonemaster/get/zones"); + + interceptor.afterCompletion(request, response, new Object(), null); + + verify(sessionObject, never()).updateSessionObject(anyString(), anyString()); + } +} diff --git a/src/test/java/com/iemr/admin/utils/http/HttpUtilsTest.java b/src/test/java/com/iemr/admin/utils/http/HttpUtilsTest.java new file mode 100644 index 0000000..7ac2353 --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/http/HttpUtilsTest.java @@ -0,0 +1,243 @@ +/* +* 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.admin.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()); + } + } +} diff --git a/src/test/java/com/iemr/admin/utils/mapper/GsonMappersTest.java b/src/test/java/com/iemr/admin/utils/mapper/GsonMappersTest.java new file mode 100644 index 0000000..9b5dfcf --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/mapper/GsonMappersTest.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.admin.utils.mapper; + +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.google.gson.Gson; +import com.google.gson.annotations.Expose; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The two mappers decide what leaves the service on the wire: the output mapper + * publishes only the fields a carrier marks {@code @Expose}, while the input + * mapper reads everything a caller sends. + */ +@DisplayName("Gson mapper Test Suite") +class GsonMappersTest { + + /** A carrier that exercises both the exposed and the hidden field handling. */ + private static final class Appointment { + @Expose + private String specialist; + @Expose + private Long appointmentId; + private String internalNote; + } + + @BeforeEach + @DisplayName("Prime the shared output builder, which only the constructor creates") + void primeOutputMapper() { + new OutputMapper(); + } + + @Nested + @DisplayName("OutputMapper") + class OutputMapperTests { + + @Test + @DisplayName("gson should publish the exposed fields and keep the hidden ones off the wire") + void gson_shouldSerialiseOnlyExposedFields() { + Appointment appointment = new Appointment(); + appointment.specialist = "dr.rao"; + appointment.internalNote = "not for the wire"; + + String json = OutputMapper.gson().toJson(appointment); + + assertTrue(json.contains("\"specialist\":\"dr.rao\""), json); + assertFalse(json.contains("internalNote"), "a field without @Expose must stay off the wire"); + } + + @Test + @DisplayName("gson should render a long as a string so large ids survive a JavaScript caller") + void gson_shouldRenderLongAsString() { + Appointment appointment = new Appointment(); + appointment.appointmentId = 9_007_199_254_740_993L; + + assertTrue(OutputMapper.gson().toJson(appointment).contains("\"9007199254740993\"")); + } + + @Test + @DisplayName("gson should serialise nulls rather than omit them") + void gson_shouldSerialiseNulls() { + assertTrue(OutputMapper.gson().toJson(new Appointment()).contains("null")); + } + + @Test + @DisplayName("gsonWithoutExpose should publish every field, annotated or not") + void gsonWithoutExpose_shouldPublishEveryField() { + Appointment appointment = new Appointment(); + appointment.specialist = "dr.rao"; + appointment.internalNote = "kept internally"; + + String json = OutputMapper.gsonWithoutExpose().toJson(appointment); + + assertTrue(json.contains("internalNote"), json); + assertTrue(json.contains("specialist"), json); + } + + @Test + @DisplayName("the constructor should reuse the shared builder across instances") + void constructor_shouldReuseSharedBuilder() { + new OutputMapper(); + + assertNotNull(OutputMapper.gson()); + } + } + + @Nested + @DisplayName("InputMapper and OutputMapper together") + class RoundTripTests { + + @Test + @DisplayName("a carrier written by the output mapper should be readable by the input mapper") + void carrier_shouldSurviveARoundTrip() { + Appointment appointment = new Appointment(); + appointment.specialist = "dr.rao"; + appointment.internalNote = "not for the wire"; + Gson writer = OutputMapper.gson(); + + Appointment restored = InputMapper.gson().fromJson(writer.toJson(appointment), Appointment.class); + + assertEquals("dr.rao", restored.specialist); + assertNull(restored.internalNote, "a hidden field must not come back over the wire"); + } + } +} diff --git a/src/test/java/com/iemr/admin/utils/mapper/InputMapperTest.java b/src/test/java/com/iemr/admin/utils/mapper/InputMapperTest.java new file mode 100644 index 0000000..4738014 --- /dev/null +++ b/src/test/java/com/iemr/admin/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.admin.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/admin/utils/redis/RedisSessionExceptionTest.java b/src/test/java/com/iemr/admin/utils/redis/RedisSessionExceptionTest.java new file mode 100644 index 0000000..261b2a4 --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/redis/RedisSessionExceptionTest.java @@ -0,0 +1,74 @@ +/* +* 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.admin.utils.redis; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.admin.utils.exception.IEMRException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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; + +@DisplayName("RedisSessionException Test Suite") +class RedisSessionExceptionTest { + + private static final String MESSAGE = "Unable to fetch session object from Redis server"; + + @Test + @DisplayName("the message constructor should expose the message through both accessors") + void messageConstructor_shouldExposeMessage() { + RedisSessionException exception = new RedisSessionException(MESSAGE); + + assertEquals(MESSAGE, exception.getMessage()); + assertEquals(MESSAGE, exception.toString()); + } + + @Test + @DisplayName("a Redis session failure should be reportable as an AMRIT exception") + void redisSessionException_shouldBeAnIemrException() { + assertTrue(new RedisSessionException(MESSAGE) instanceof IEMRException); + } + + @Test + @DisplayName("the cause constructor should adopt the stack trace of the cause without chaining it") + void causeConstructor_shouldAdoptCauseStackTrace() { + RuntimeException cause = new RuntimeException("connection refused"); + cause.setStackTrace(new StackTraceElement[] { + new StackTraceElement("com.iemr.Origin", "connect", "Origin.java", 42) }); + + RedisSessionException exception = new RedisSessionException(MESSAGE, cause); + + assertEquals(MESSAGE, exception.getMessage()); + assertArrayEquals(cause.getStackTrace(), exception.getStackTrace()); + assertNull(exception.getCause()); + } + + @Test + @DisplayName("the Redis connection marker type should be instantiable") + void redisConnectionMarker_shouldBeInstantiable() { + assertNotNull(new RedisConnection()); + } +} diff --git a/src/test/java/com/iemr/admin/utils/redis/RedisStorageTest.java b/src/test/java/com/iemr/admin/utils/redis/RedisStorageTest.java new file mode 100644 index 0000000..af40210 --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/redis/RedisStorageTest.java @@ -0,0 +1,189 @@ +/* +* 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.admin.utils.redis; + +import java.nio.charset.StandardCharsets; + +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.connection.RedisConnection; +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; + +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; + +@ExtendWith(MockitoExtension.class) +@DisplayName("RedisStorage Test Suite") +class RedisStorageTest { + + private static final String KEY = "session-key"; + private static final int EXPIRY_SECONDS = 7200; + + @Mock + private LettuceConnectionFactory connectionFactory; + + @Mock + private RedisConnection redisConnection; + + private RedisStorage redisStorage; + + @BeforeEach + @DisplayName("Wire the store with a mocked Lettuce connection factory before each test") + void setUp() { + redisStorage = new RedisStorage(); + ReflectionTestUtils.setField(redisStorage, "connection", connectionFactory); + when(connectionFactory.getConnection()).thenReturn(redisConnection); + } + + private byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + @Nested + @DisplayName("setObject") + class SetObjectTests { + + @Test + @DisplayName("setObject should write the value when no session is stored yet") + void setObject_shouldWriteValueWhenKeyIsAbsent() throws RedisSessionException { + when(redisConnection.get(bytes(KEY))).thenReturn(null); + + assertEquals(KEY, redisStorage.setObject(KEY, "payload", EXPIRY_SECONDS)); + verify(redisConnection).set(eq(bytes(KEY)), eq(bytes("payload")), + eq(Expiration.seconds(EXPIRY_SECONDS)), eq(SetOption.UPSERT)); + } + + @Test + @DisplayName("setObject should write the value when the stored session is empty") + void setObject_shouldWriteValueWhenStoredSessionIsEmpty() throws RedisSessionException { + when(redisConnection.get(bytes(KEY))).thenReturn(bytes("")); + + assertEquals(KEY, redisStorage.setObject(KEY, "payload", EXPIRY_SECONDS)); + verify(redisConnection).set(any(byte[].class), any(byte[].class), any(Expiration.class), any(SetOption.class)); + } + + @Test + @DisplayName("setObject should leave an existing session untouched") + void setObject_shouldLeaveExistingSessionUntouched() throws RedisSessionException { + when(redisConnection.get(bytes(KEY))).thenReturn(bytes("existing")); + + assertEquals(KEY, redisStorage.setObject(KEY, "payload", EXPIRY_SECONDS)); + verify(redisConnection, never()).set(any(byte[].class), any(byte[].class), + any(Expiration.class), any(SetOption.class)); + } + } + + @Nested + @DisplayName("getObject") + class GetObjectTests { + + @Test + @DisplayName("getObject should return the stored session and extend its expiry") + void getObject_shouldReturnStoredSessionAndExtendExpiry() throws RedisSessionException { + when(redisConnection.get(bytes(KEY))).thenReturn(bytes("payload")); + + assertEquals("payload", redisStorage.getObject(KEY, true, EXPIRY_SECONDS)); + verify(redisConnection).expire(bytes(KEY), EXPIRY_SECONDS); + } + + @Test + @DisplayName("getObject should raise a session exception when the key is absent") + void getObject_shouldRaiseWhenKeyIsAbsent() { + when(redisConnection.get(bytes(KEY))).thenReturn(null); + + RedisSessionException thrown = assertThrows(RedisSessionException.class, + () -> redisStorage.getObject(KEY, true, EXPIRY_SECONDS)); + + assertEquals("Unable to fetch session object from Redis server", thrown.getMessage()); + } + + @Test + @DisplayName("getObject should raise a session exception when the stored value is blank") + void getObject_shouldRaiseWhenStoredValueIsBlank() { + when(redisConnection.get(bytes(KEY))).thenReturn(bytes(" ")); + + assertThrows(RedisSessionException.class, + () -> redisStorage.getObject(KEY, true, EXPIRY_SECONDS)); + verify(redisConnection, never()).expire(any(byte[].class), any(Long.class)); + } + } + + @Nested + @DisplayName("updateObject") + class UpdateObjectTests { + + @Test + @DisplayName("updateObject should overwrite an existing session") + void updateObject_shouldOverwriteExistingSession() throws RedisSessionException { + when(redisConnection.get(bytes(KEY))).thenReturn(bytes("old")); + + assertEquals(KEY, redisStorage.updateObject(KEY, "new", true, EXPIRY_SECONDS)); + verify(redisConnection).set(eq(bytes(KEY)), eq(bytes("new")), + eq(Expiration.seconds(EXPIRY_SECONDS)), eq(SetOption.UPSERT)); + } + + @Test + @DisplayName("updateObject should raise a session exception when there is nothing to update") + void updateObject_shouldRaiseWhenKeyIsAbsent() { + when(redisConnection.get(bytes(KEY))).thenReturn(null); + + RedisSessionException thrown = assertThrows(RedisSessionException.class, + () -> redisStorage.updateObject(KEY, "new", true, EXPIRY_SECONDS)); + + assertEquals("Unable to fetch session object from Redis server", thrown.getMessage()); + } + } + + @Nested + @DisplayName("deleteObject") + class DeleteObjectTests { + + @Test + @DisplayName("deleteObject should return the number of keys Redis removed") + void deleteObject_shouldReturnNumberOfKeysRemoved() throws RedisSessionException { + when(redisConnection.del(bytes(KEY))).thenReturn(1L); + + assertEquals(1L, redisStorage.deleteObject(KEY)); + } + + @Test + @DisplayName("deleteObject should return zero when the key was not present") + void deleteObject_shouldReturnZeroWhenKeyAbsent() throws RedisSessionException { + when(redisConnection.del(bytes(KEY))).thenReturn(0L); + + assertEquals(0L, redisStorage.deleteObject(KEY)); + } + } +} diff --git a/src/test/java/com/iemr/admin/utils/response/OutputResponseTest.java b/src/test/java/com/iemr/admin/utils/response/OutputResponseTest.java new file mode 100644 index 0000000..6a99924 --- /dev/null +++ b/src/test/java/com/iemr/admin/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.admin.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.admin.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() throws Exception { + 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() throws Exception { + assertNull(outputResponse.getData()); + } + } + + @Nested + @DisplayName("setResponse") + class SetResponseTests { + + @Test + @DisplayName("setResponse should mark the response successful") + void setResponse_shouldMarkResponseSuccessful() throws Exception { + 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() throws Exception { + outputResponse.setResponse("{\"specialistAvailabilityID\":12345}"); + + assertTrue(outputResponse.getData().contains("\"specialistAvailabilityID\"")); + assertTrue(outputResponse.getData().startsWith("{")); + } + + @Test + @DisplayName("setResponse should keep a JSON array payload as an array") + void setResponse_shouldKeepJsonArrayPayload() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + outputResponse.setError(new NullPointerException("npe")); + + assertEquals(OutputResponse.CODE_EXCEPTION, outputResponse.getStatusCode()); + } + + @Test + @DisplayName("setError should map ParseException to a code exception") + void setError_shouldMapParseExceptionToCodeException() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + 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() throws Exception { + outputResponse.setError(new SQLGrammarException("bad column")); + + assertEquals(OutputResponse.CODE_EXCEPTION, outputResponse.getStatusCode()); + } + + @Test + @DisplayName("setError should fall back to a generic failure for an unmapped constraint violation") + void setError_shouldMapConstraintViolation() throws Exception { + outputResponse.setError(new ConstraintViolationException("duplicate key")); + + assertEquals(OutputResponse.GENERIC_FAILURE, outputResponse.getStatusCode()); + } + } +} diff --git a/src/test/java/com/iemr/admin/utils/sessionobject/SessionObjectTest.java b/src/test/java/com/iemr/admin/utils/sessionobject/SessionObjectTest.java new file mode 100644 index 0000000..c7e41f5 --- /dev/null +++ b/src/test/java/com/iemr/admin/utils/sessionobject/SessionObjectTest.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.admin.utils.sessionobject; + +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 com.iemr.admin.utils.config.ConfigProperties; +import com.iemr.admin.utils.redis.RedisSessionException; +import com.iemr.admin.utils.redis.RedisStorage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyBoolean; +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; + +@ExtendWith(MockitoExtension.class) +@DisplayName("SessionObject Test Suite") +class SessionObjectTest { + + private static final String KEY = "session-key"; + private static final String VALUE = "{\"userName\":\"amrit-user\"}"; + + @Mock + private RedisStorage objectStore; + + private SessionObject sessionObject; + private int expectedExpiry; + private boolean expectedExtend; + + @BeforeEach + @DisplayName("Wire the session holder with a mocked Redis store before each test") + void setUp() { + sessionObject = new SessionObject(); + sessionObject.setObjectStore(objectStore); + expectedExpiry = ConfigProperties.getSessionExpiryTime(); + expectedExtend = ConfigProperties.getExtendExpiryTime(); + } + + @Nested + @DisplayName("Reading and writing the session") + class ReadWriteTests { + + @Test + @DisplayName("getSessionObject should delegate to the store with the configured expiry settings") + void getSessionObject_shouldDelegateWithConfiguredExpiry() throws RedisSessionException { + when(objectStore.getObject(KEY, expectedExtend, expectedExpiry)).thenReturn(VALUE); + + assertEquals(VALUE, sessionObject.getSessionObject(KEY)); + verify(objectStore).getObject(KEY, expectedExtend, expectedExpiry); + } + + @Test + @DisplayName("setSessionObject should delegate to the store with the configured expiry") + void setSessionObject_shouldDelegateWithConfiguredExpiry() throws RedisSessionException { + when(objectStore.setObject(KEY, VALUE, expectedExpiry)).thenReturn(KEY); + + assertEquals(KEY, sessionObject.setSessionObject(KEY, VALUE)); + verify(objectStore).setObject(KEY, VALUE, expectedExpiry); + } + + @Test + @DisplayName("updateSessionObject should delegate to the store with the configured expiry settings") + void updateSessionObject_shouldDelegateWithConfiguredExpiry() throws RedisSessionException { + when(objectStore.updateObject(KEY, VALUE, expectedExtend, expectedExpiry)).thenReturn(KEY); + + assertEquals(KEY, sessionObject.updateSessionObject(KEY, VALUE)); + verify(objectStore).updateObject(KEY, VALUE, expectedExtend, expectedExpiry); + } + + @Test + @DisplayName("deleteSessionObject should delegate the removal to the store") + void deleteSessionObject_shouldDelegateRemoval() throws RedisSessionException { + when(objectStore.deleteObject(KEY)).thenReturn(1L); + + sessionObject.deleteSessionObject(KEY); + + verify(objectStore).deleteObject(KEY); + } + } + + @Nested + @DisplayName("Propagating store failures") + class FailureTests { + + @Test + @DisplayName("getSessionObject should propagate a missing-session failure") + void getSessionObject_shouldPropagateMissingSessionFailure() throws RedisSessionException { + when(objectStore.getObject(anyString(), anyBoolean(), anyInt())) + .thenThrow(new RedisSessionException("Unable to fetch session object from Redis server")); + + RedisSessionException thrown = assertThrows(RedisSessionException.class, + () -> sessionObject.getSessionObject(KEY)); + + assertEquals("Unable to fetch session object from Redis server", thrown.getMessage()); + } + + @Test + @DisplayName("updateSessionObject should propagate a missing-session failure") + void updateSessionObject_shouldPropagateMissingSessionFailure() throws RedisSessionException { + when(objectStore.updateObject(eq(KEY), eq(VALUE), anyBoolean(), anyInt())) + .thenThrow(new RedisSessionException("Unable to fetch session object from Redis server")); + + assertThrows(RedisSessionException.class, () -> sessionObject.updateSessionObject(KEY, VALUE)); + } + + @Test + @DisplayName("deleteSessionObject should propagate a store failure") + void deleteSessionObject_shouldPropagateStoreFailure() throws RedisSessionException { + when(objectStore.deleteObject(KEY)).thenThrow(new RedisSessionException("redis down")); + + assertThrows(RedisSessionException.class, () -> sessionObject.deleteSessionObject(KEY)); + } + } +}