From ef70e6e0159d2090bf64aae1d874512018160d56 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Mon, 2 Nov 2026 15:40:28 +0530 Subject: [PATCH 1/3] Merge to main release 3.12.0 (#205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add clinical staff information * fix: pom version * feat(stoptb): add Nikshay ID Generator CSV export/import Adds a date-range-scoped export of Stop TB beneficiaries as a CSV formatted for the Nikshay ID Generator app, and an import endpoint to write the portal-generated Nikshay IDs back onto those beneficiaries. Scoped by visit-date range only, no vanID/servicePointID — MMU runs one local database per van, so everything in it already belongs to the current van/service point. Location columns (village/healthFacility/tu/district/state) are resolved against Nikshay's own, isolated location hierarchy (m_nikshay_village -> m_nikshay_facility -> m_nikshay_tu -> m_nikshay_district -> m_nikshay_state), not AMRIT's standard masters — a Stop TB beneficiary's I_bendemographics.DistrictBranchID actually holds their Nikshay Village ID (the same overload Common-API's NikshayAddressResolver relies on), so every other location field is derived by walking up that one chain. A beneficiary whose village doesn't resolve all the way to a state is silently left out of the export, same as one who already has a Nikshay ID. New endpoints: - GET /stopTb/nikshay/exportBeneficiariesCsv?fromDate=&toDate= - POST /stopTb/nikshay/importResultsCsv?visitDate=&file= Co-Authored-By: Claude Sonnet 5 * fix(stoptb): store Nikshay ID on tb_suspected, match import rows by phone+name Two corrections after checking real data: 1. nikshay_id now lives on tb_suspected, not tb_stoptb_diagnostics — both tables have the column, but tb_suspected is the one this feature is meant to use going forward. Both the export's already-has-an-ID check and the import's write path now target tb_suspected.benRegID. 2. A real Nikshay ID Generator results file has no beneficiary ID column of any kind — only the original template columns plus its own generatedId/status/durationSec/error. The import can no longer assume benRegId round-trips through untouched. Rows are now matched back to a beneficiary by normalized primaryPhone + case-insensitive firstName (NikshayExportRepository.findMatchingBeneficiaryIds); anything other than exactly one match is left for manual review rather than guessed. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): rewrite export query against verified real schema Every table the export previously used for beneficiary/visit data turned out not to exist on a real server (i_beneficiary, I_bendemographics, tb_stoptb_visit) — confirmed via BadSqlGrammarException in production logs and direct schema inspection. Rewritten against tables and columns verified with real data: - Visit filter: t_benvisitdetail (VisitCategory = 'Stop TB'), not tb_stoptb_visit — confirmed 511 real rows vs. zero. - Beneficiary identity: MMU's datasource only connects to db_iemr, but beneficiary data lives in db_identity on the same physical server — reached via fully-qualified cross-schema table names: i_beneficiarymapping (BenRegId, unique) -> BenDetailsId -> i_beneficiarydetails (name/DOB/gender/caste/occupation/income/HIV, already denormalized, no extra master-table joins needed) and BenAddressId -> i_beneficiaryaddress (address/village/pincode), plus BenContactsId -> i_beneficiarycontacts (phone). - New confirmed real gap: many van-registered beneficiaries (i_beneficiarymapping.CreatedBy = 'reglocal') have a mapping row but their detail/address rows never synced to db_identity, sometimes for days. These are now silently skipped (no FirstName resolved), same category as an unresolved location — renamed countUnresolvedLocation -> countNotReadyToExport to reflect both cases. - Import's phone+name matching updated to the same verified join path. Tested against real data on a live server: 15 of 20 today's-date beneficiaries correctly skipped (identity never synced), 5 correctly resolved end-to-end including the full Nikshay location chain, producing valid CSV rows. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): fix CSV streaming response (No converter for [...Lambda...]) exportBeneficiariesCsv was declared ResponseEntity so it could return either a StreamingResponseBody (success) or a plain String (bad request/ error). Spring's StreamingResponseBodyReturnValueHandler decides whether to special-case the response based on the method's *declared* generic return type, not the runtime object — a wildcard resolves to Object, StreamingResponseBody isn't assignable from Object, so it falls through to normal HttpMessageConverters, which can't serialize a raw lambda: "No converter for [...NikshayExportController$$Lambda...] with preset Content-Type 'text/csv'" (confirmed in production logs, 2026-08-18). Fixed by keeping the return type consistently ResponseEntity and wrapping error messages as a StreamingResponseBody that writes the message as bytes, instead of returning a plain String on any path. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): allow REGISTRAR role on Nikshay export/import endpoints @PreAuthorize only listed clinical roles (Nurse/Doctor/Pharmacist/Lab Technician/etc.), copied from sn/nikshaya's original controller without verifying against real Stop TB camp roles. Confirmed in production: AccessDeniedException for a real Stop TB user (stoptb1) whose roles (checked directly via v_userservicerolemapping) include Registrar, Nurse, Doctor, Pharmacist, Lab Technician, Data Sync — Registrar was the one missing from the allow-list. Downloading/uploading a beneficiary CSV is fundamentally a registrar task, so this was very likely the actual active session role denied by AccessDeniedException in the logs. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): remove @PreAuthorize role gate on Nikshay export/import Role-based gating proved fragile — wrong role list on the first attempt, still AccessDeniedException after adding REGISTRAR. SecurityConfig's anyRequest().authenticated() still requires a logged-in user; dropping the role-specific check per explicit request rather than guessing a third time. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): write CSV synchronously instead of via StreamingResponseBody Root cause of the persistent AccessDeniedException (survived removing @PreAuthorize entirely): StreamingResponseBody makes Spring process the response body on a Servlet async re-dispatch. Confirmed from production logs (2026-08-18) — two AccessDeniedExceptions per request, the second via ApplicationDispatcher/AsyncContextImpl. Spring Security's own core filter chain is registered for ASYNC dispatch by default and re-runs, but this app's custom JwtUserIdValidationFilter/RoleAuthenticationFilter (which read the JWT cookie and establish who's logged in) are not — AnonymousAuthenticationFilter fires instead on the second pass, and the global anyRequest().authenticated() rule denies it. Fixed by writing the CSV directly and synchronously onto HttpServletResponse instead of returning a StreamingResponseBody — avoids async dispatch entirely, so there's no second security pass to fail. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): read phone/address from the columns actually populated Verified against real data: PhoneNum1 and CurrAddressValue are empty on every real beneficiary checked — the actual data lives in PreferredPhoneNum (phone) and CurrAddrLine1/2/3 + CurrHabitation (address), confirmed by direct query on real records. Falls back through PhoneNum1/PhoneNum2 if PreferredPhoneNum is empty. Address line parts are individually NULLIF'd before CONCAT_WS, since blank (not NULL) line fields otherwise leave stray ", ," artifacts. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): import phone matching missed PreferredPhoneNum too findMatchingBeneficiaryIds only checked c.PhoneNum1 — same bug already fixed on the export side (commit 79d8422d) but missed here. Confirmed in production: two real beneficiaries (17877 'K', 17875 'U') with PhoneNum1 IS NULL but PreferredPhoneNum populated ('9999999999') both came back "no beneficiary found" on upload. Now checks PreferredPhoneNum/PhoneNum1/PhoneNum2, same as the export's column preference order. Verified against real data: 17877 now matches correctly for phone '9999999999' + firstName 'K'. Co-Authored-By: Claude Sonnet 5 * feat(stoptb): use benRegId directly when present, fall back to phone+name If a results row carries a parseable benRegId (e.g. a manually-rebuilt test file, or a future ID Generator version that keeps it), use it directly — exact match, no ambiguity possible, skips the phone+name lookup entirely. Falls back to the existing phone+name matching only when benRegId is absent or unparseable, which is what a genuine Nikshay portal results file looks like (confirmed no ID column at all). Makes test files with benRegId (e.g. a rebuilt export) match instantly and unambiguously, while real portal files keep working exactly as before via the phone+name path. Co-Authored-By: Claude Sonnet 5 * fix: add the endpoints and schema for downsync from central to local * fix: downsync implementation * fix: build issue * fix: lastmoddate updation * fix: update the response of downsync * fix: update the continuous call on check sync progress * fix: column name issue for stoptb tables * fix: issue in stoptb tables * fix: add central id * fix: FK Mapping issue * fix: downsync issue * fix: FK issue in i_ beneficiarymapping table * fix: include U for downsync again * fix: revert central id for m_beneficiaryregidmapping * fix: conflict section * fix: manage conflicts and add F flag for resync * fix: pom version update * fix: add the condition to block the download from different van * fix: resolve foreign key violation * fix: retry failed records * fix: update 'F' flag * fix: sonarqube comments * fix: resolve sonarcube comments * fix: issue in m_beneficiaryregidmapping table * Adding api's for syncing diagnostic documents to s3 * fix: add the endpoints and schema for downsync from central to local * fix: downsync implementation * fix: build issue * fix: lastmoddate updation * fix: update the response of downsync * fix: update the continuous call on check sync progress * fix: column name issue for stoptb tables * fix: issue in stoptb tables * fix: add central id * fix: FK Mapping issue * fix: downsync issue * fix: FK issue in i_ beneficiarymapping table * fix: include U for downsync again * fix: revert central id for m_beneficiaryregidmapping * fix: conflict section * fix: manage conflicts and add F flag for resync * fix: pom version update * fix: add the condition to block the download from different van * fix: resolve foreign key violation * fix: retry failed records * fix: update 'F' flag * fix: sonarqube comments * fix: add clinical staff information * fix: pom version * fix: resolve sonarcube comments * fix: issue in m_beneficiaryregidmapping table * Adding api's for syncing diagnostic documents to s3 * Adding api's for syncing diagnostic documents to s3 * feat(stoptb): record whether AMRIT caused each Nikshay ID to be created The results-CSV import already wrote the portal's Nikshay ID onto tb_suspected, but nothing distinguished an ID the portal generated for us from one that already existed there and was only matched back. Counting "IDs created by this application" therefore meant inferring it from the ID's presence, which over-counts every pre-existing ID. Each written row now also sets tb_suspected.nikshay_created_by_amrit -- true for a "success" row, false for a "skipped" row (portal-detected duplicate). Rows left for manual review or genuinely failed still write nothing at all, so the existing matching, review and failure behaviour is unchanged; "failed" maps to false for completeness only, as those rows have no ID to write. The import response gains createdByAmrit/alreadyOnNikshay counts, a breakdown of the existing `updated` total that always sums to it, so the figure is available without a query. Additive -- no existing field changes meaning. Requires AMRIT-DB V107 for the column. Note the flag is not yet in the van<->central sync column mapping, so it stays van-local until that row is updated. Co-Authored-By: Claude Opus 5 (1M context) * chore(stoptb): remove added code comments Co-Authored-By: Claude Opus 5 (1M context) * fix(stoptb): store Nikshay ID on i_beneficiarydetails, not tb_suspected The import previously fabricated a brand-new tb_suspected row for any beneficiary who didn't already have one, just to hold the generated Nikshay ID. tb_suspected is a clinical table (is_confirmed, is_referred, reason_for_suspicion, etc.) — inserting a bare row there falsely implied the beneficiary had been assessed as a TB suspect when no such assessment ever happened, and every beneficiary needs a Nikshay ID regardless of suspect status. A Nikshay ID is a beneficiary-level identifier, not a clinical fact, so it now lives on db_identity.i_beneficiarydetails (nikshayId, nikshayCreatedByAmrit — new columns, migration to follow separately in AMRIT-DB) alongside similar existing identifiers like rchid on i_beneficiarydetails_rmnch. - NikshayExportRepository: existingNikshayId now reads d.nikshayId (already-joined i_beneficiarydetails) instead of a tb_suspected subquery. Replaced findLatestSuspectedId/updateNikshayId/ insertSuspectedWithNikshayId with a single writeNikshayId() — always an UPDATE via i_beneficiarymapping.BenRegId -> BenDetailsId, never an INSERT, since every beneficiary reaching this point already has a synced i_beneficiarydetails row. - NikshayImportService: calls writeNikshayId() directly and reports a failed row if it updates zero rows, instead of silently dropping the generated ID. tb_suspected is no longer touched by this feature. Co-Authored-By: Claude Sonnet 5 * Dropping duplicate columns in table tb_stoptb_general_examination * Merge 3.4.2 with 3.9.0 (#193) * fix: Added proper space in the string * Fix the issue in saving ANC Category Beneficiary (#146) * fix: anc care issue * fix: alignment * fix: datasync issue * fix: datasync issue * fix: sync timeout issue * Release 3.6.0 to Release-3.6.1 (#159) * fix: amm-2147 anc care visit is not saving in the nurse section * fix: amm-2147 anc care visit is not saving in the nurse section * Revert "Sn/3.6.0" * fix: amm-2147 anc care visit is not saving in the nurse section * docs: update README and sync package version with pom (#169) --------- Co-authored-by: SnehaRH Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: Arpit Tripathi Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> * Adding s3 dependency * Making improvements given by coderabbit * Made changes in pom release * Use vanID instead of villageId in diagnostic document S3 path (#197) The S3 key was prefixed with a villageId taken from the request param, so every document in a push run was filed under one village regardless of where it came from. Use each row's own vanID instead; villageId is now optional on /diagnostic-documents-to-server and ignored. Co-authored-by: Claude Opus 5.5 * Vb/diagnostic document sync response (#198) * Use vanID instead of villageId in diagnostic document S3 path The S3 key was prefixed with a villageId taken from the request param, so every document in a push run was filed under one village regardless of where it came from. Use each row's own vanID instead; villageId is now optional on /diagnostic-documents-to-server and ignored. Co-Authored-By: Claude Opus 5.5 * Return van-to-server style summary from diagnostic document push The push always answered "Data successfully synced" when at least one document went through, hiding partial failures. Return the same table-level shape /van-to-server uses (status success/partial/failed, totalRecords, successfulRecords, failedRecords, failureReasons) so the data sync screen can show what failed and why. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 * fix: update the file extension * feat(STOP-457): automatic BenID download for StopTB laptops The laptop now downloads BeneficiaryIDs from central by itself, so StopTB registration no longer depends on someone clicking "Generate BenID". - CentralAuthService: after the operator's normal login, signs in to central once as an API client and keeps only the refresh token (AES-GCM encrypted, key tied to this machine, file 600 under /opt/amrit/data). Each run uses refreshToken -> JWT -> getLoginResponse to get a central session key. No password is stored; a password central rejected is never sent again. - BenIdAutoTopUpService: pool 0 -> 10000, below 2000 -> 1000. IDs received from central are written to a pending file before the local import, so a failed import is retried with the same IDs. Retries 5/15/30/60 min; one lock shared with the manual "Generate BenID" button. - /centralAuth/status and /centralAuth/capture for MMU-UI. - Top-up check also runs after a successful van-to-server upload and master download. - Only active when stoptb.enforce.vanid=true and stoptb.van.id>0; every setting has a default, so other deployments need no config change. No change on central (Common-API / BenGen). Co-Authored-By: Claude Opus 5.5 * fix(STOP-457): harden automatic BenID download for many laptops - Ask central for at most 1000 IDs per request (benid.pool.maxPerRequest): a 10000-ID request can outlast the central gateway timeout, and every retry after a lost reply reserved another 10000 unused IDs. - Remember a central-rejected laptop password across restarts (marker file with the user name only), so daily restarts cannot slowly lock the central account; a successful Data Sync reconnect clears it. - Write the pending-import file atomically (temp + move); a pending file that still fails after 3 tries is set aside so top-ups never get stuck. - Spread retry waits by +/-20% so laptops do not all hit central at once after an outage. - Run background work on the feature's own single thread instead of the shared ForkJoin pool. Tests: 26 unit tests pass; checked in the running app (2500 IDs arrived as 1000+1000+500; rejected login not resent after restart). Co-Authored-By: Claude Opus 5.5 * chore: set MMU-API version to 3.12.0 Co-Authored-By: Claude Opus 5.5 * StopTB camp laptop data sync – = sync, inventory upload fixes and master download (#201) * feat(AMM-2491): automatic data sync and sync failure review for StopTB laptops - Upload (van to central) every 30 min and master download every 6 h, using the central connection of the automatic BenID download (no stored password; SyncedBy = operator who connected the laptop). Own thread and lock; skips while a manual master download runs. - GET /dataSyncActivity/autoSyncStatus: last runs, last error, and rows central rejected (Processed='F') per table and SyncFailureReason. - Active only where stoptb.enforce.vanid=true and stoptb.van.id>0; manual Upload / Download Masters unchanged. 6 unit tests. Co-Authored-By: Claude Opus 5.5 * fix(AMM-2491): send sync failure time as text Co-Authored-By: Claude Opus 5.5 * feat(AMM-2491): automatic inventory data download after each upload Refills and stock issued at central for this van's store (indents, indent issues, stock transfers, stock entries) reach the laptop without pressing Inventory Data Download. Runs after each automatic upload; its own failure does not undo the upload; shown in autoSyncStatus. Switch: sync.autoInventoryDownload.enabled (default true). 2 new unit tests. Co-Authored-By: Claude Opus 5.5 * fix(datasync): inventory data download skipped indent orders and stock transfers The loop over the 5 tables also did i++ at the end of each pass, so only t_indent, t_indentissue and t_itemstockentry were downloaded; t_indentorder and t_stocktransfer never reached the van. Co-Authored-By: Claude Opus 5.5 * fix(datasync): central keeps the van row's VanSerialNo and store on upload m_synctabledetail maps van columns to server columns by position (the van's own ID sits where the server list has VanSerialNo); since the by-name record mapping, VanSerialNo was stored NULL, so a re-sent row was inserted again (duplicates) and joins on VanSerialNo broke. VanSerialNo now takes the van row ID and SyncFacilityID the row's store when not sent by name (dataSync.fillVanKeys, default true). Master download on central logs the row count instead of every row (the 661k-row village table exhausted the heap). Co-Authored-By: Claude Opus 5.5 * feat(AMM-2491): StopTB laptop master download and automatic sync off by default - StopTB laptops (stoptb.enforce.vanid=true): service-line and van masters (hubs, vans, stores, items, item-store mapping, roles) are downloaded in full for the van's line, so a laptop set up from a copied database gets them all; shared masters (villages) stay incremental. Other MMU laptops unchanged. - Automatic upload / master download / inventory download are off by default (manual sync until approved); when on, the automatic master download fetches only the inventory masters (sync.autoMasterDownload.inventoryOnly=true). Co-Authored-By: Claude Opus 5.5 * fix(datasync): central links uploaded rows to central visit/prescription IDs A van sends its own IDs in BenVisitID / PrescriptionID; on central those numbers belong to other rows, so prescriptions and prescribed drugs were linked to another van's visit/prescription, or rejected by the foreign key when the number did not exist. With dataSync.translateVanLinks=true the link is replaced by the central ID of the row the same van uploaded with that VanSerialNo; if that row is not on central yet the record is not saved and is sent again on the next upload. Off by default (other MMU servers unchanged); to be switched on for the StopTB central. Co-Authored-By: Claude Opus 5.5 * fix(datasync): StopTB laptop uploads stock tables only through the Inventory group Inventory tables are configured in group 5 (old lists: stock-outs without FacilityID/SyncFacilityID, patient issues without SyncFacilityID, batch insert fails) and in group 9 Inventory (complete). Group 5 runs first and marks the rows uploaded, so central stored stock-outs without their camp store and per-store reports missed them. On StopTB laptops (stoptb.enforce.vanid=true) a table that a later group also uploads is skipped. Other MMU laptops unchanged. Co-Authored-By: Claude Opus 5.5 * fix(datasync): StopTB laptop upload uses the renamed reason_to_close column and continues past a failing table The laptop sync list still names tb_diagnostic_order.reason_for_refusal; V125 renamed it to reason_to_close and the whole upload stopped with bad SQL grammar. The column name the table really has is used for the van and server column lists, and a table that cannot be read is recorded as failed while the other tables still upload. Both only on a StopTB laptop (stoptb.enforce.vanid). Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 * fix(downsync): create worklist rows for down-synced beneficiaries (#203) * fix(downsync): create worklist rows for down-synced beneficiaries Down-sync copies the beneficiary tables but not i_ben_flow_outreach, and FLW-API getBeneficiaryData serves tabs only beneficiaries that have a worklist row. Beneficiaries registered elsewhere and down-synced to the laptop therefore never reached the laptop's other tabs, so tab counts differed. After each down-sync, insert the missing worklist row for this van's beneficiaries, filled as TM-API does at registration. Processed = 'P' so it is not uploaded back (central already has its own row); modified_by = 'downsync' marks these rows. A failure here is logged and does not undo the down-sync. Co-Authored-By: Claude Opus 5.5 * fix(s3): let vans start without AWS keys — only central needs them (#202) Vans (camp laptops) run the same war as central but never call S3: they push diagnostic documents to central over HTTP. Yet startup failed with "Could not resolve placeholder 'AWS_S3_ACCESS_KEY'", and blank values would still crash in AwsBasicCredentials.create ("Access key ID cannot be blank"). - common_docker.properties: default the S3 placeholders (keys/bucket empty, region ap-south-1, push batch size 3). - S3ClientConfig: with blank keys use DefaultCredentialsProvider, which resolves lazily on first S3 call instead of at startup. Central keeps setting real AWS_S3_* values and behaves as before. Co-authored-by: Claude Opus 5.5 (cherry picked from commit 10b8ffaea199506b88811d3228877b37aaada550) --------- Co-authored-by: Claude Opus 5.5 --------- Co-authored-by: vanitha1822 Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: vishwab1 Co-authored-by: Claude Sonnet 5 Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: Sneha Co-authored-by: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Co-authored-by: SnehaRH Co-authored-by: Arpit Tripathi --- pom.xml | 9 +- src/main/environment/common_ci.properties | 19 +- src/main/environment/common_docker.properties | 27 +- .../environment/common_example.properties | 40 +- src/main/java/com/iemr/mmu/Application.java | 2 + .../com/iemr/mmu/config/S3ClientConfig.java | 53 +++ .../benid/CentralAuthController.java | 94 ++++ .../dataSyncActivity/StartSyncActivity.java | 77 +++- .../MMUDataSyncVanToServer.java | 23 +- .../repo/stoptb/NikshayExportRepository.java | 6 +- .../SyncUtilityClassRepo.java | 2 + .../service/benid/BenIdAutoTopUpService.java | 372 ++++++++++++++++ .../mmu/service/benid/CentralAuthService.java | 416 ++++++++++++++++++ .../dataSyncActivity/AutoSyncService.java | 239 ++++++++++ .../dataSyncActivity/DataSyncRepository.java | 108 +++++ .../DownSyncDataFromServerImpl.java | 18 + .../DownloadDataFromServerImpl.java | 38 ++ ...wnloadDataFromServerTransactionalImpl.java | 2 - .../UploadDataToServerImpl.java | 89 +++- .../DataSyncRepositoryCentral.java | 16 + .../DataSyncRepositoryCentralDownload.java | 3 +- .../DiagnosticDocumentFetchService.java | 62 +++ .../DiagnosticDocumentIngestService.java | 139 ++++++ .../DiagnosticDocumentPushServiceImpl.java | 274 ++++++++++++ .../DiagnosticDocumentRepository.java | 95 ++++ .../GetDataFromVanAndSyncToDBImpl.java | 83 +++- .../java/com/iemr/mmu/utils/CryptoUtil.java | 42 ++ .../benid/BenIdAutoTopUpServiceTest.java | 272 ++++++++++++ .../service/benid/CentralAuthServiceTest.java | 276 ++++++++++++ .../dataSyncActivity/AutoSyncServiceTest.java | 176 ++++++++ .../DownloadMastersFullForLineTest.java | 49 +++ .../UploadRenamedColumnTest.java | 48 ++ .../UploadSkipDuplicateGroupTest.java | 51 +++ .../GetDataFromVanAndSyncToDBVanKeysTest.java | 71 +++ ...GetDataFromVanAndSyncToDBVanLinksTest.java | 101 +++++ 35 files changed, 3373 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/iemr/mmu/config/S3ClientConfig.java create mode 100644 src/main/java/com/iemr/mmu/controller/benid/CentralAuthController.java create mode 100644 src/main/java/com/iemr/mmu/service/benid/BenIdAutoTopUpService.java create mode 100644 src/main/java/com/iemr/mmu/service/benid/CentralAuthService.java create mode 100644 src/main/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncService.java create mode 100644 src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentFetchService.java create mode 100644 src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentIngestService.java create mode 100644 src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentPushServiceImpl.java create mode 100644 src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentRepository.java create mode 100644 src/main/java/com/iemr/mmu/utils/CryptoUtil.java create mode 100644 src/test/java/com/iemr/mmu/service/benid/BenIdAutoTopUpServiceTest.java create mode 100644 src/test/java/com/iemr/mmu/service/benid/CentralAuthServiceTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncServiceTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadMastersFullForLineTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadRenamedColumnTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadSkipDuplicateGroupTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBVanKeysTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBVanLinksTest.java diff --git a/pom.xml b/pom.xml index dd4e04a0..5749c203 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.mmu mmu-api - 3.9.0 + 3.12.0 war MMU-API @@ -294,7 +294,12 @@ h2 runtime - + + + software.amazon.awssdk + s3 + 2.25.60 + org.apache.commons commons-csv diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index faf5180f..cd33f081 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -5,6 +5,12 @@ spring.datasource.username=@env.DATABASE_USERNAME@ spring.datasource.password=@env.DATABASE_PASSWORD@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +## S3 storage for diagnostic documents pushed from a van +aws.s3.access-key=@env.AWS_S3_ACCESS_KEY@ +aws.s3.secret-key=@env.AWS_S3_SECRET_KEY@ +aws.s3.region=@env.AWS_S3_REGION@ +diagnostic.documents.s3.bucket=@env.DIAGNOSTIC_DOCUMENTS_S3_BUCKET@ + ## Carestream URLs, local carestreamOrderCreateURL=@env.COMMON_API@carestream/createOrder @@ -28,6 +34,11 @@ dataSyncUploadUrl=@env.MMU_CENTRAL_SERVER@dataSync/van-to-server ## Data download API, central dataSyncDownloadUrl=@env.MMU_CENTRAL_SERVER@dataSync/server-to-van +## Diagnostic document push (this server -> further central server) +diagnosticDocumentUploadUrl=@env.MMU_CENTRAL_SERVER@dataSync/diagnostic-documents +diagnosticDocument.push.batchSize=@env.DIAGNOSTIC_DOCUMENT_PUSH_BATCH_SIZE@ +diagnostic.documents.storage-root=@env.DIAGNOSTIC_DOCUMENTS_STORAGE_ROOT@ + ## TC specialist slot booking, # not required tcSpecialistSlotBook=@env.TM_API@schedule/bookSlot @@ -95,4 +106,10 @@ logging.file.name=@env.MMU_API_LOGGING_FILE_NAME@ springdoc.api-docs.enabled=@env.SWAGGER_DOC_ENABLED@ springdoc.swagger-ui.enabled=@env.SWAGGER_DOC_ENABLED@ -cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ \ No newline at end of file +cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ + +## BenID auto top-up URLs (feature stays off: stoptb.* not set for these builds) +benCheckUrlLocal=@env.IDENTITY_API_BASE_URL@id/checkAvailablBenIDLocalServer +centralLoginUrl=@env.COMMON_API_OPEN_SYNC@user/userAuthenticate +centralRefreshTokenUrl=@env.COMMON_API_OPEN_SYNC@user/refreshToken +centralSessionUrl=@env.COMMON_API_OPEN_SYNC@user/getLoginResponse diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 28f6b89e..05a361a2 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -5,6 +5,12 @@ spring.datasource.username=${DATABASE_USERNAME} spring.datasource.password=${DATABASE_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +## S3 storage for diagnostic documents pushed from a van +aws.s3.access-key=${AWS_S3_ACCESS_KEY:} +aws.s3.secret-key=${AWS_S3_SECRET_KEY:} +aws.s3.region=${AWS_S3_REGION:ap-south-1} +diagnostic.documents.s3.bucket=${DIAGNOSTIC_DOCUMENTS_S3_BUCKET:} + ## Carestream URLs, local carestreamOrderCreateURL=${COMMON_API}carestream/createOrder @@ -28,6 +34,11 @@ dataSyncUploadUrl=${MMU_CENTRAL_SERVER}dataSync/van-to-server ## Data download API, central dataSyncDownloadUrl=${MMU_CENTRAL_SERVER}dataSync/server-to-van +## Diagnostic document push (this server -> further central server) +diagnosticDocumentUploadUrl=${MMU_CENTRAL_SERVER}dataSync/diagnostic-documents +diagnosticDocument.push.batchSize=${DIAGNOSTIC_DOCUMENT_PUSH_BATCH_SIZE:3} +diagnostic.documents.storage-root=${DIAGNOSTIC_DOCUMENTS_STORAGE_ROOT} + ## TC specialist slot booking, # not required tcSpecialistSlotBook=${TM_API}schedule/bookSlot @@ -93,4 +104,18 @@ logging.path=logs logging.file.name=${MMU_API_LOGGING_FILE_NAME} springdoc.api-docs.enabled=${SWAGGER_DOC_ENABLED} -springdoc.swagger-ui.enabled=${SWAGGER_DOC_ENABLED} \ No newline at end of file +springdoc.swagger-ui.enabled=${SWAGGER_DOC_ENABLED} + +## BenID auto top-up (StopTB laptops only; off unless STOPTB_ENFORCE_VANID=true and STOPTB_VAN_ID>0) +benCheckUrlLocal=${IDENTITY_API_BASE_URL}id/checkAvailablBenIDLocalServer +centralLoginUrl=${COMMON_API_OPEN_SYNC}user/userAuthenticate +centralRefreshTokenUrl=${COMMON_API_OPEN_SYNC}user/refreshToken +centralSessionUrl=${COMMON_API_OPEN_SYNC}user/getLoginResponse +stoptb.enforce.vanid=${STOPTB_ENFORCE_VANID:false} +stoptb.van.id=${STOPTB_VAN_ID:0} +benid.pool.lowerLimit=${BENID_POOL_LOWER_LIMIT:2000} +benid.pool.batchSize=${BENID_POOL_BATCH_SIZE:1000} +benid.pool.bootstrapSize=${BENID_POOL_BOOTSTRAP_SIZE:10000} +benid.token.dir=${BENID_TOKEN_DIR:/opt/amrit/data} +benid.autoTopUp.initialDelayMs=${BENID_AUTO_TOPUP_INITIAL_DELAY_MS:120000} +benid.autoTopUp.checkIntervalMs=${BENID_AUTO_TOPUP_CHECK_INTERVAL_MS:300000} diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 294c0e45..46996a54 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -7,6 +7,12 @@ spring.datasource.username= spring.datasource.password= spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +## S3 storage for diagnostic documents pushed from a van (POST /dataSync/diagnostic-documents) +aws.s3.access-key= +aws.s3.secret-key= +aws.s3.region=ap-south-1 +diagnostic.documents.s3.bucket= + ## Carestream URLs, local #carestreamOrderCreateURL =http://localhost:1040/carestream/createOrder carestreamOrderCreateURL =http://localhost:8083/carestream/createOrder @@ -44,6 +50,10 @@ dataSyncUploadUrl=http://10.208.122.38:8080/mmu-api/dataSync/van-to-server #dataSyncDownloadUrl=http://localhost:82/dataSync/server-to-van dataSyncDownloadUrl=http://10.208.122.38:8080/mmu-api/dataSync/server-to-van +diagnosticDocumentUploadUrl=http://localhost:8087/mmu-api/dataSync/diagnostic-documents +diagnosticDocument.push.batchSize=3 +diagnostic.documents.storage-root= + ## TC specialist slot booking, # not required #tcSpecialistSlotBook=http://localhost:8080/schedule/bookSlot tcSpecialistSlotBook=http://10.208.122.38:8080/tm-api/schedule/bookSlot @@ -110,4 +120,32 @@ logging.path=logs/ logging.file.name=logs/mmu-api.log -cors.allowed-origins=http://localhost:* \ No newline at end of file +cors.allowed-origins=http://localhost:* + +## BenID auto top-up (StopTB laptops only) +benCheckUrlLocal=http://localhost:8094/id/checkAvailablBenIDLocalServer +centralLoginUrl=https://uatstoptb.piramalswasthya.org/common-api/user/userAuthenticate +centralRefreshTokenUrl=https://uatstoptb.piramalswasthya.org/common-api/user/refreshToken +centralSessionUrl=https://uatstoptb.piramalswasthya.org/common-api/user/getLoginResponse +stoptb.enforce.vanid=false +stoptb.van.id=0 +benid.pool.lowerLimit=2000 +benid.pool.batchSize=1000 +benid.pool.bootstrapSize=10000 +benid.token.dir=/opt/amrit/data +benid.autoTopUp.initialDelayMs=120000 +benid.autoTopUp.checkIntervalMs=300000 + +## Automatic Data Sync (StopTB laptops only; needs the BenID central connection above). Off by default: manual sync only. +sync.autoUpload.enabled=false +sync.autoUpload.intervalMs=1800000 +sync.autoMasterDownload.enabled=false +sync.autoMasterDownload.intervalMs=21600000 +# automatic master download fetches only inventory masters (items, stores, item->store mapping); false = all masters +sync.autoMasterDownload.inventoryOnly=true +sync.autoInventoryDownload.enabled=false + +## Central upload: replace van IDs in link columns (BenVisitID, PrescriptionID) by central IDs. +## Off by default (other MMU servers unchanged); on for the StopTB central. +## Env alternative: DATASYNC_TRANSLATEVANLINKS=true +dataSync.translateVanLinks=false diff --git a/src/main/java/com/iemr/mmu/Application.java b/src/main/java/com/iemr/mmu/Application.java index 712d34f5..8fb40ccf 100644 --- a/src/main/java/com/iemr/mmu/Application.java +++ b/src/main/java/com/iemr/mmu/Application.java @@ -29,11 +29,13 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.scheduling.annotation.EnableScheduling; import com.iemr.mmu.data.login.Users; import com.iemr.mmu.utils.IEMRApplBeans; @SpringBootApplication +@EnableScheduling public class Application { public static void main(String[] args) { diff --git a/src/main/java/com/iemr/mmu/config/S3ClientConfig.java b/src/main/java/com/iemr/mmu/config/S3ClientConfig.java new file mode 100644 index 00000000..eb3509bf --- /dev/null +++ b/src/main/java/com/iemr/mmu/config/S3ClientConfig.java @@ -0,0 +1,53 @@ +package com.iemr.mmu.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; + +/*** + * @purpose S3 client beans used to store diagnostic documents pushed from a van. Built + * once and shared, rather than re-built per request. + */ +@Configuration +public class S3ClientConfig { + + @Value("${aws.s3.access-key}") + private String accessKey; + + @Value("${aws.s3.secret-key}") + private String secretKey; + + @Value("${aws.s3.region}") + private String region; + + /* + * Only the central server talks to S3; a van (camp laptop) runs the same war but never + * calls it, so it must be able to start without AWS keys. AwsBasicCredentials rejects blank + * keys at construction time, so fall back to the SDK default chain, which is resolved lazily + * on the first S3 call rather than at startup. + */ + private AwsCredentialsProvider credentialsProvider() { + if (accessKey == null || accessKey.isBlank() || secretKey == null || secretKey.isBlank()) { + return DefaultCredentialsProvider.create(); + } + return StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)); + } + + @Bean + public S3Client diagnosticDocumentS3Client() { + return S3Client.builder().region(Region.of(region)).credentialsProvider(credentialsProvider()).build(); + } + + @Bean + public S3Presigner diagnosticDocumentS3Presigner() { + return S3Presigner.builder().region(Region.of(region)).credentialsProvider(credentialsProvider()).build(); + } +} diff --git a/src/main/java/com/iemr/mmu/controller/benid/CentralAuthController.java b/src/main/java/com/iemr/mmu/controller/benid/CentralAuthController.java new file mode 100644 index 00000000..71533a5d --- /dev/null +++ b/src/main/java/com/iemr/mmu/controller/benid/CentralAuthController.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.mmu.controller.benid; + +import org.json.JSONObject; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.iemr.mmu.service.benid.BenIdAutoTopUpService; +import com.iemr.mmu.service.benid.CentralAuthService; +import com.iemr.mmu.utils.response.OutputResponse; + +import io.swagger.v3.oas.annotations.Operation; + +@RestController +@RequestMapping(value = "/centralAuth", headers = "Authorization", produces = "application/json") +public class CentralAuthController { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + @Autowired + private CentralAuthService centralAuthService; + + @Autowired + private BenIdAutoTopUpService benIdAutoTopUpService; + + @Operation(summary = "Automatic BenID top-up and central connection status") + @GetMapping(value = "/status") + public String status() { + OutputResponse response = new OutputResponse(); + try { + response.setResponse(benIdAutoTopUpService.status().toString()); + } catch (Exception e) { + logger.error("Error reading BenID auto top-up status: " + e.getMessage()); + response.setError(e); + } + return response.toString(); + } + + /** + * Called by MMU-UI right after a successful login. Returns immediately; the + * central login and any top-up happen in the background. + */ + @Operation(summary = "Connect this laptop to central with the user's login (one time)") + @PostMapping(value = "/capture", consumes = "application/json") + public String capture(@RequestBody String requestOBJ) { + OutputResponse response = new OutputResponse(); + try { + if (!centralAuthService.isEnabled()) { + response.setResponse("disabled"); + return response.toString(); + } + JSONObject obj = new JSONObject(requestOBJ); + String userName = obj.optString("userName", null); + String password = obj.optString("password", null); + boolean force = obj.optBoolean("reconnect", false); + benIdAutoTopUpService.runInBackground(() -> { + if (centralAuthService.capture(userName, password, force)) { + benIdAutoTopUpService.triggerNow(); + } + }); + response.setResponse("accepted"); + } catch (Exception e) { + logger.error("Error in central capture: " + e.getMessage()); + response.setError(e); + } + return response.toString(); + } +} diff --git a/src/main/java/com/iemr/mmu/controller/dataSyncActivity/StartSyncActivity.java b/src/main/java/com/iemr/mmu/controller/dataSyncActivity/StartSyncActivity.java index a7f2a7c7..973fdf41 100644 --- a/src/main/java/com/iemr/mmu/controller/dataSyncActivity/StartSyncActivity.java +++ b/src/main/java/com/iemr/mmu/controller/dataSyncActivity/StartSyncActivity.java @@ -21,6 +21,8 @@ */ package com.iemr.mmu.controller.dataSyncActivity; +import java.util.Map; + import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,13 +33,18 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.google.gson.Gson; +import com.iemr.mmu.service.benid.BenIdAutoTopUpService; import com.iemr.mmu.service.dataSyncActivity.DownSyncDataFromServerImpl; import com.iemr.mmu.service.dataSyncActivity.DownloadDataFromServerImpl; import com.iemr.mmu.service.dataSyncActivity.DownloadDataFromServerTransactionalImpl; +import com.iemr.mmu.service.dataSyncActivity.AutoSyncService; import com.iemr.mmu.service.dataSyncActivity.UploadDataToServerImpl; +import com.iemr.mmu.service.dataSyncLayerCentral.DiagnosticDocumentFetchService; +import com.iemr.mmu.service.dataSyncLayerCentral.DiagnosticDocumentPushServiceImpl; import com.iemr.mmu.utils.CookieUtil; import com.iemr.mmu.utils.response.OutputResponse; @@ -60,7 +67,18 @@ public class StartSyncActivity { @Autowired private DownloadDataFromServerTransactionalImpl downloadDataFromServerTransactionalImpl; @Autowired + private DiagnosticDocumentPushServiceImpl diagnosticDocumentPushServiceImpl; + @Autowired + private DiagnosticDocumentFetchService diagnosticDocumentFetchService; + @Autowired private DownSyncDataFromServerImpl downSyncDataFromServerImpl; + + @Autowired + private BenIdAutoTopUpService benIdAutoTopUpService; + + @Autowired + private AutoSyncService autoSyncService; + private static final String GROUP_ID = "groupID"; private static final String PROVIDER_SERVICE_MAP_ID = "providerServiceMapID"; @@ -79,6 +97,8 @@ public String dataSyncToServer(@RequestBody String requestOBJ, serverAuthorization, jwtToken); // if (s != null) response.setResponse(s); + // internet is up right now: top up the BenID pool if it is low + benIdAutoTopUpService.triggerNow(); // else // response.setError(5000, "Error in data sync"); } else { @@ -91,6 +111,45 @@ public String dataSyncToServer(@RequestBody String requestOBJ, return response.toStringWithSerialization(); } + @Operation(summary = "Push pending diagnostic documents (docsProcessed='N') to the further central server, in batches") + @PostMapping(value = { "/diagnostic-documents-to-server" }) + public String diagnosticDocumentsToServer(@RequestHeader(value = "Authorization") String authorization, + @RequestHeader(value = "ServerAuthorization") String serverAuthorization, + @RequestParam(required = false) Long villageId) { + OutputResponse response = new OutputResponse(); + try { + String s = diagnosticDocumentPushServiceImpl.pushPendingDocuments(serverAuthorization); + if (s != null) + response.setResponse(s); + else + response.setError(5000, "Error in diagnostic document push"); + } catch (Exception e) { + logger.error("Error in diagnostic document push : " + e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Fetch a short-lived download URL for the latest successfully-pushed diagnostic document matching a beneficiary+documentType, from this van's own local record") + @GetMapping(value = { "/diagnostic-documents/download" }) + public String diagnosticDocumentDownloadUrl(@RequestParam Long beneficiaryId, @RequestParam String documentType, + @RequestHeader(value = "Authorization") String authorization) { + OutputResponse response = new OutputResponse(); + try { + Map download = diagnosticDocumentFetchService.getLatestDocumentDownload(beneficiaryId, + documentType); + if (download != null) + response.setResponse(new Gson().toJson(download)); + else + response.setError(5000, "No pushed document found for beneficiaryId=" + beneficiaryId + + ", documentType=" + documentType); + } catch (Exception e) { + logger.error("Error fetching diagnostic document download URL : " + e); + response.setError(e); + } + return response.toString(); + } + @Operation(summary = "Get data sync group details") @GetMapping(value = { "/getSyncGroupDetails" }) public String getSyncGroupDetails() { @@ -131,8 +190,11 @@ public String startMasterDownload(@RequestBody String requestOBJ, if (s.equalsIgnoreCase("inProgress")) response.setError(5000, "Download is already in progress from different device, kindly wait to finish this"); - else + else { response.setResponse(s); + // internet is up right now: top up the BenID pool if it is low + benIdAutoTopUpService.triggerNow(); + } } else response.setError(5000, s); } else { @@ -147,6 +209,19 @@ public String startMasterDownload(@RequestBody String requestOBJ, return response.toString(); } + @Operation(summary = "Automatic data sync status and the rows central rejected on upload") + @GetMapping(value = { "/autoSyncStatus" }) + public String autoSyncStatus() { + OutputResponse response = new OutputResponse(); + try { + response.setResponse(autoSyncService.status().toString()); + } catch (Exception e) { + logger.error("Error while getting automatic sync status : " + e); + response.setError(e); + } + return response.toString(); + } + @Operation(summary = "Data sync master download progress check") @GetMapping(value = { "/checkMastersDownloadProgress" }) public String checkMastersDownloadProgress() { diff --git a/src/main/java/com/iemr/mmu/controller/dataSyncLayerCentral/MMUDataSyncVanToServer.java b/src/main/java/com/iemr/mmu/controller/dataSyncLayerCentral/MMUDataSyncVanToServer.java index 589e1dcb..1d2c8790 100644 --- a/src/main/java/com/iemr/mmu/controller/dataSyncLayerCentral/MMUDataSyncVanToServer.java +++ b/src/main/java/com/iemr/mmu/controller/dataSyncLayerCentral/MMUDataSyncVanToServer.java @@ -24,7 +24,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -35,11 +34,11 @@ import com.iemr.mmu.data.syncActivity_syncLayer.DownSyncDataDigester; import com.iemr.mmu.data.syncActivity_syncLayer.SyncDownloadMaster; import com.iemr.mmu.data.syncActivity_syncLayer.SyncUploadDataDigester; +import com.iemr.mmu.service.dataSyncLayerCentral.DiagnosticDocumentIngestService; import com.iemr.mmu.service.dataSyncLayerCentral.FetchDownloadDataImpl; import com.iemr.mmu.service.dataSyncLayerCentral.GetDataFromVanAndSyncToDBImpl; import com.iemr.mmu.service.dataSyncLayerCentral.GetDownSyncDataFromCentralImpl; import com.iemr.mmu.service.dataSyncLayerCentral.GetMasterDataFromCentralForVanImpl; -import com.iemr.mmu.utils.CookieUtil; import com.iemr.mmu.utils.response.OutputResponse; import io.swagger.v3.oas.annotations.Operation; @@ -61,6 +60,8 @@ public class MMUDataSyncVanToServer { private FetchDownloadDataImpl fetchDownloadDataImpl; @Autowired private GetDownSyncDataFromCentralImpl getDownSyncDataFromCentralImpl; + @Autowired + private DiagnosticDocumentIngestService diagnosticDocumentIngestService; @Operation(summary = "Sync data from van-to-server") @PostMapping(value = { "/van-to-server" }, consumes = "application/json", produces = "application/json") @@ -81,6 +82,24 @@ public String dataSyncToServer(@RequestBody String requestOBJ, return response.toString(); } + @Operation(summary = "Receive diagnostic documents pushed from a van and store each in S3 (no database write here)") + @PostMapping(value = { "/diagnostic-documents" }, consumes = "application/json", produces = "application/json") + public String diagnosticDocumentsFromVan(@RequestBody String requestOBJ, + @RequestHeader(value = "Authorization") String Authorization) { + OutputResponse response = new OutputResponse(); + try { + String s = diagnosticDocumentIngestService.ingestDocuments(requestOBJ); + if (s != null) + response.setResponse(s); + else + response.setError(5000, "diagnostic document ingest failed"); + } catch (Exception e) { + response.setError(e); + logger.error("Diagnostic document ingest Exception" + e); + } + return response.toString(); + } + @Operation(summary = "Download data from server-to-van") @PostMapping(value = { "/server-to-van" }, consumes = "application/json", produces = "application/json") public String dataDownloadFromServer(@RequestBody SyncDownloadMaster syncDownloadMaster, diff --git a/src/main/java/com/iemr/mmu/repo/stoptb/NikshayExportRepository.java b/src/main/java/com/iemr/mmu/repo/stoptb/NikshayExportRepository.java index 5302188d..fa3bd7f5 100644 --- a/src/main/java/com/iemr/mmu/repo/stoptb/NikshayExportRepository.java +++ b/src/main/java/com/iemr/mmu/repo/stoptb/NikshayExportRepository.java @@ -146,8 +146,10 @@ public record NikshayRawRow(Long benRegId, String firstName, String middleLastNa + " d.incomeStatus AS socioeconomicStatus, " + " (SELECT o.chief_complaint FROM tb_stoptb_general_opd o WHERE o.ben_reg_id = m.BenRegId " + " AND o.deleted = 0 ORDER BY o.id DESC LIMIT 1) AS chiefComplaint, " - + " (SELECT ge.hiv_status FROM tb_stoptb_general_examination ge WHERE ge.beneficiary_reg_id = m.BenRegId " - + " AND ge.deleted = 0 ORDER BY ge.id DESC LIMIT 1) AS hivStatus, " + // hiv_status used to live on tb_stoptb_general_examination too, but that copy was + // dropped (AMRIT-DB V109) once tb_screening became the single source of truth. + + " (SELECT s.hiv_status FROM tb_screening s WHERE s.ben_reg_id = m.BenRegId " + + " AND (s.deleted = 0 OR s.deleted IS NULL) ORDER BY s.id DESC LIMIT 1) AS hivStatus, " + " d.IsHIVPositive AS isHivPos, " + " d.nikshayId AS existingNikshayId " + "FROM db_identity.i_beneficiarymapping m " diff --git a/src/main/java/com/iemr/mmu/repo/syncActivity_syncLayer/SyncUtilityClassRepo.java b/src/main/java/com/iemr/mmu/repo/syncActivity_syncLayer/SyncUtilityClassRepo.java index c146a7a9..dd054c41 100644 --- a/src/main/java/com/iemr/mmu/repo/syncActivity_syncLayer/SyncUtilityClassRepo.java +++ b/src/main/java/com/iemr/mmu/repo/syncActivity_syncLayer/SyncUtilityClassRepo.java @@ -31,4 +31,6 @@ @Repository public interface SyncUtilityClassRepo extends CrudRepository { List findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(Integer groupID, Boolean deleted); + + List findByDeletedOrderBySyncTableDetailID(Boolean deleted); } diff --git a/src/main/java/com/iemr/mmu/service/benid/BenIdAutoTopUpService.java b/src/main/java/com/iemr/mmu/service/benid/BenIdAutoTopUpService.java new file mode 100644 index 00000000..56b78f16 --- /dev/null +++ b/src/main/java/com/iemr/mmu/service/benid/BenIdAutoTopUpService.java @@ -0,0 +1,372 @@ +/* +* 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.mmu.service.benid; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.locks.ReentrantLock; + +import org.json.JSONArray; +import org.json.JSONObject; +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.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.iemr.mmu.utils.RestTemplateUtil; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +/** + * Keeps the local BenID pool filled without anyone clicking "Generate BenID". + * + * Runs only on StopTB laptops (stoptb.enforce.vanid=true and stoptb.van.id>0). + * Pool empty -> bootstrap size; pool below lower limit -> batch size. IDs + * received from central are written to a pending file before the local import, + * so a failed import is retried with the same IDs instead of reserving new ones. + */ +@Service +public class BenIdAutoTopUpService { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + /** Shared with the manual "Generate BenID" button so both never run together. */ + public static final ReentrantLock GENERATION_LOCK = new ReentrantLock(); + + private static final String PENDING_FILE_NAME = "benid-pending-import.json"; + private static final long TOKEN_KEEP_ALIVE_MS = 24 * 60 * 60_000L; + + @Autowired + private CentralAuthService centralAuthService; + + @Value("${benGenUrlCentral:}") + private String benGenUrlCentral; + + @Value("${benImportUrlLocal:}") + private String benImportUrlLocal; + + @Value("${benCheckUrlLocal:}") + private String benCheckUrlLocal; + + @Value("${benid.pool.lowerLimit:2000}") + private long lowerLimit; + + @Value("${benid.pool.batchSize:1000}") + private long batchSize; + + @Value("${benid.pool.bootstrapSize:10000}") + private long bootstrapSize; + + // central is asked for at most this many IDs per request: a big request can outlast the + // central gateway timeout, and a reply lost after central created the IDs wastes them + @Value("${benid.pool.maxPerRequest:1000}") + private long maxPerRequest; + + @Value("${benid.token.dir:/opt/amrit/data}") + private String dataDir; + + // wait after the 1st, 2nd, 3rd and later failures (5, 15, 30, 60 minutes) + @Value("${benid.autoTopUp.retryDelaysMs:300000,900000,1800000,3600000}") + private long[] retryDelaysMs; + + @Value("${benid.http.connectTimeoutMs:15000}") + private int connectTimeoutMs; + + @Value("${benid.http.readTimeoutMs:60000}") + private int readTimeoutMs; + + private RestTemplate restTemplate; + + private volatile int failureCount; + private volatile long nextAttemptAt; + private volatile long lastPoolCount = -1; + private volatile long lastTopUpAt; + private volatile long lastTopUpCount; + private volatile String lastError; + private volatile int pendingImportFailures; + + // one background thread for this feature, so network waits never hold shared pool threads + private final ExecutorService background = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "benid-auto-topup"); + t.setDaemon(true); + return t; + }); + + private static final int MAX_PENDING_IMPORT_FAILURES = 3; + + @PostConstruct + public void init() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(connectTimeoutMs); + factory.setReadTimeout(readTimeoutMs); + restTemplate = new RestTemplate(factory); + } + + @PreDestroy + public void shutdown() { + background.shutdownNow(); + } + + /** Runs a task on this feature's own background thread. */ + public void runInBackground(Runnable task) { + background.submit(task); + } + + /** Regular check; does nothing on central or non-StopTB deployments. */ + @Scheduled(initialDelayString = "${benid.autoTopUp.initialDelayMs:120000}", fixedDelayString = "${benid.autoTopUp.checkIntervalMs:300000}") + public void scheduledCheck() { + runOnce(false); + } + + /** Run straight away (after a login or a Data Sync), in the background. */ + public void triggerNow() { + if (centralAuthService.isEnabled()) { + background.submit(() -> runOnce(true)); + } + } + + private void runOnce(boolean ignoreBackoff) { + if (!centralAuthService.isEnabled()) { + return; + } + if (!ignoreBackoff && System.currentTimeMillis() < nextAttemptAt) { + return; + } + if (!GENERATION_LOCK.tryLock()) { + logger.info("BenID generation already running - skipping this check"); + return; + } + try { + // IDs already received but not saved locally are imported first + if (importPendingIfAny()) { + onSuccess(); + } + + long available = countLocalPool(); + lastPoolCount = available; + long needed = available <= 0 ? bootstrapSize : (available < lowerLimit ? batchSize : 0); + if (needed == 0) { + centralAuthService.keepAliveIfDue(TOKEN_KEEP_ALIVE_MS); + onSuccess(); + return; + } + + String key = centralAuthService.getCentralSessionKey(); + if (key == null) { + lastError = "No central connection - waiting for a user login"; + logger.warn("BenID pool low ({} for vanID {}) but no central token - waiting for a user login", + available, centralAuthService.getVanID()); + return; + } + + int imported = 0; + for (long remaining = needed; remaining > 0;) { + long chunk = Math.min(remaining, Math.max(1, maxPerRequest)); + imported += generateAndImport(centralAuthService.getVanID(), chunk, key, key, "datasync"); + remaining -= chunk; + } + lastPoolCount = countLocalPool(); + lastTopUpAt = System.currentTimeMillis(); + lastTopUpCount = imported; + logger.info("BenID auto top-up: vanID {} +{} IDs, pool now {}", centralAuthService.getVanID(), imported, + lastPoolCount); + onSuccess(); + } catch (Exception e) { + onFailure(e); + } finally { + GENERATION_LOCK.unlock(); + } + } + + /** + * Asks central for IDs for the van and imports them locally. The central + * response is written to the pending file first, so a failed local import + * does not lose IDs that central has already reserved for this van. + * + * @return number of IDs imported + */ + public int generateAndImport(int vanID, long count, String centralAuthorization, String localAuthorization, + String localToken) throws Exception { + JSONObject request = new JSONObject(); + request.put("vanID", vanID); + request.put("benIDRequired", count); + + HttpEntity centralRequest = RestTemplateUtil.createRequestEntity(request.toString(), + centralAuthorization, "datasync"); + logger.info("Requesting {} BenIDs from central for vanID {}", count, vanID); + long start = System.currentTimeMillis(); + ResponseEntity response = restTemplate.exchange(benGenUrlCentral, HttpMethod.POST, centralRequest, + String.class); + logger.info("Central BenID generation answered in {} ms", System.currentTimeMillis() - start); + JSONObject obj = new JSONObject(response.getBody()); + if (obj.optInt("statusCode") != 200 || !obj.has("data")) { + throw new IllegalStateException("Central BenID generation failed: " + obj.optString("errorMessage")); + } + JSONArray ids = obj.getJSONArray("data"); + for (int k = 0; k < ids.length(); k++) { + ids.getJSONObject(k).put("vanID", vanID); + } + writePending(ids.toString()); + long importStart = System.currentTimeMillis(); + importLocally(ids.toString(), localAuthorization, localToken); + deletePending(); + logger.info("Imported {} BenIDs locally in {} ms", ids.length(), System.currentTimeMillis() - importStart); + return ids.length(); + } + + private boolean importPendingIfAny() throws Exception { + Path pending = pendingPath(); + if (!Files.exists(pending)) { + return false; + } + logger.info("Importing BenIDs left from an earlier run"); + try { + String payload = new String(Files.readAllBytes(pending), StandardCharsets.UTF_8); + importLocally(payload, "datasync", "datasync"); + } catch (Exception e) { + pendingImportFailures++; + if (pendingImportFailures < MAX_PENDING_IMPORT_FAILURES) { + throw e; + } + // a file that keeps failing (e.g. half-written at a power cut) must not stop top-ups + Path setAside = Paths.get(dataDir, "benid-pending-import.failed-" + System.currentTimeMillis() + ".json"); + Files.move(pending, setAside, StandardCopyOption.REPLACE_EXISTING); + pendingImportFailures = 0; + logger.warn("Pending BenID file could not be imported {} times; moved to {} and continuing: {}", + MAX_PENDING_IMPORT_FAILURES, setAside, e.getMessage()); + return false; + } + pendingImportFailures = 0; + deletePending(); + return true; + } + + private void importLocally(String payload, String authorization, String token) { + HttpEntity localRequest = RestTemplateUtil.createRequestEntity(payload, authorization, token); + ResponseEntity response = restTemplate.exchange(benImportUrlLocal, HttpMethod.POST, localRequest, + String.class); + JSONObject obj = new JSONObject(response.getBody()); + if (obj.optInt("statusCode") != 200) { + throw new IllegalStateException("Local BenID import failed: " + obj.optString("errorMessage")); + } + } + + public long countLocalPool() { + HttpEntity request = RestTemplateUtil.createRequestEntity(null, "datasync", "datasync"); + ResponseEntity response = restTemplate.exchange(benCheckUrlLocal, HttpMethod.GET, request, + String.class); + JSONObject obj = new JSONObject(response.getBody()); + if (obj.optInt("statusCode") != 200) { + throw new IllegalStateException("Local BenID count failed: " + obj.optString("errorMessage")); + } + Object data = obj.get("data"); + JSONObject dataObj = data instanceof JSONObject ? (JSONObject) data : new JSONObject(data.toString()); + return Long.parseLong(dataObj.get("response").toString()); + } + + private void onSuccess() { + failureCount = 0; + nextAttemptAt = 0; + lastError = null; + } + + private void onFailure(Exception e) { + long base = retryDelaysMs[Math.min(failureCount, retryDelaysMs.length - 1)]; + // +/-20% so that many laptops do not all retry at the same moment after an outage + long delay = base <= 0 ? 0 : (long) (base * ThreadLocalRandom.current().nextDouble(0.8, 1.2)); + failureCount++; + nextAttemptAt = System.currentTimeMillis() + delay; + lastError = e.getMessage(); + logger.warn("BenID auto top-up failed (attempt {}), next try in {} s: {}", failureCount, delay / 1000, + e.getMessage()); + } + + private Path pendingPath() { + return Paths.get(dataDir, PENDING_FILE_NAME); + } + + private void writePending(String payload) { + try { + Files.createDirectories(Paths.get(dataDir)); + // write a temp file and swap it in, so a power cut never leaves a half-written file + Path tmp = Paths.get(dataDir, PENDING_FILE_NAME + ".tmp"); + Files.write(tmp, payload.getBytes(StandardCharsets.UTF_8)); + try { + Files.move(tmp, pendingPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (java.nio.file.AtomicMoveNotSupportedException e) { + Files.move(tmp, pendingPath(), StandardCopyOption.REPLACE_EXISTING); + } + } catch (Exception e) { + logger.warn("Could not write pending BenID file: {}", e.getMessage()); + } + } + + private void deletePending() { + try { + Files.deleteIfExists(pendingPath()); + } catch (Exception e) { + logger.warn("Could not delete pending BenID file: {}", e.getMessage()); + } + } + + // ---------------------------------------------------------------- status + + public JSONObject status() { + JSONObject s = new JSONObject(); + s.put("enabled", centralAuthService.isEnabled()); + if (!centralAuthService.isEnabled()) { + return s; + } + s.put("vanID", centralAuthService.getVanID()); + s.put("centralConnection", centralAuthService.getState().name()); + s.put("connectedUser", centralAuthService.getConnectedUser()); + try { + lastPoolCount = countLocalPool(); + } catch (Exception e) { + logger.debug("Live BenID count not available: {}", e.getMessage()); + } + s.put("poolCount", lastPoolCount); + s.put("lowerLimit", lowerLimit); + s.put("lastTopUpAt", lastTopUpAt); + s.put("lastTopUpCount", lastTopUpCount); + s.put("pendingImport", Files.exists(pendingPath())); + String error = lastError != null ? lastError : centralAuthService.getLastError(); + if (error != null) { + s.put("lastError", error); + } + return s; + } +} diff --git a/src/main/java/com/iemr/mmu/service/benid/CentralAuthService.java b/src/main/java/com/iemr/mmu/service/benid/CentralAuthService.java new file mode 100644 index 00000000..d065eb19 --- /dev/null +++ b/src/main/java/com/iemr/mmu/service/benid/CentralAuthService.java @@ -0,0 +1,416 @@ +/* +* 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.mmu.service.benid; + +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import jakarta.annotation.PostConstruct; + +/** + * Keeps this laptop connected to the central server without storing any + * central password. + * + * The operator's normal laptop login is passed here once; it is used for a + * single central login as an API client, which returns a refresh token. Only + * that refresh token is kept (AES-GCM encrypted with a key tied to this + * machine). Each time a central session is needed the refresh token is + * exchanged for a new JWT + refresh token, and the JWT for a central session + * key. + */ +@Service +public class CentralAuthService { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + public enum State { + DISABLED, NOT_CONNECTED, CONNECTED, RECONNECT_NEEDED, PASSWORD_MISMATCH + } + + // Central refresh tokens are only issued to API (mobile) clients. + private static final String API_CLIENT_USER_AGENT = "okhttp/4.12.0"; + private static final String TOKEN_FILE_NAME = "central-refresh-token"; + // user name only (no password): a laptop password central rejected is not sent again, + // also after a restart, so the central account cannot get locked by repeated attempts + private static final String REJECTED_FILE_NAME = "central-login-rejected"; + + @Value("${stoptb.enforce.vanid:false}") + private boolean enforceVanID; + + @Value("${stoptb.van.id:0}") + private int vanID; + + @Value("${centralLoginUrl:}") + private String centralLoginUrl; + + @Value("${centralRefreshTokenUrl:}") + private String centralRefreshTokenUrl; + + @Value("${centralSessionUrl:}") + private String centralSessionUrl; + + @Value("${benid.token.dir:/opt/amrit/data}") + private String tokenDir; + + @Value("${benid.token.secret:amrit-mmu-benid}") + private String tokenSecret; + + @Value("${benid.http.connectTimeoutMs:15000}") + private int connectTimeoutMs; + + @Value("${benid.http.readTimeoutMs:60000}") + private int readTimeoutMs; + + private RestTemplate restTemplate; + + // in-memory copy; the file is only a durable backup of this value + private volatile String refreshToken; + private volatile State state = State.NOT_CONNECTED; + private volatile String lastError; + private volatile long lastRefreshAt; + private volatile String connectedUser; + private volatile String rejectedUser; + + @PostConstruct + public void init() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(connectTimeoutMs); + factory.setReadTimeout(readTimeoutMs); + restTemplate = new RestTemplate(factory); + + if (!isEnabled()) { + state = State.DISABLED; + logger.info("BenID auto top-up disabled (stoptb.enforce.vanid={}, stoptb.van.id={})", enforceVanID, + vanID); + return; + } + refreshToken = readTokenFile(); + rejectedUser = readRejectedUser(); + state = refreshToken != null ? State.CONNECTED : State.NOT_CONNECTED; + logger.info("BenID auto top-up enabled for vanID {}; central token {}", vanID, + refreshToken != null ? "loaded" : "not present yet"); + } + + public boolean isEnabled() { + return enforceVanID && vanID > 0 && !centralLoginUrl.isEmpty(); + } + + public int getVanID() { + return vanID; + } + + public boolean hasToken() { + return refreshToken != null; + } + + public State getState() { + return state; + } + + public String getLastError() { + return lastError; + } + + public long getLastRefreshAt() { + return lastRefreshAt; + } + + public String getConnectedUser() { + return connectedUser; + } + + /** + * One central login with the operator's credentials (password already + * encrypted the way MMU-UI sends it to Common-API). Skipped when a working + * token already exists, so central is not hit on every laptop login. A + * rejected password is never retried, so the central account cannot get + * locked by this. + * + * @param force true when the user explicitly reconnects (e.g. Data Sync + * login), even if a token exists + * @return true if a new token was saved + */ + public synchronized boolean capture(String userName, String encryptedPassword, boolean force) { + if (!isEnabled() || userName == null || encryptedPassword == null) { + return false; + } + if (!force && refreshToken != null && state == State.CONNECTED) { + return false; + } + // a laptop password central already rejected is not sent again (account lock risk); + // only a Data Sync login (force, real central password) tries again + if (!force && userName.trim().equalsIgnoreCase(rejectedUser)) { + logger.info("Central login skipped for user {}: password was rejected before", userName); + return false; + } + try { + JSONObject body = new JSONObject(); + body.put("userName", userName.trim()); + body.put("password", encryptedPassword); + body.put("doLogout", false); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set(HttpHeaders.USER_AGENT, API_CLIENT_USER_AGENT); + + ResponseEntity response = restTemplate.exchange(centralLoginUrl, HttpMethod.POST, + new HttpEntity<>(body.toString(), headers), String.class); + JSONObject obj = new JSONObject(response.getBody()); + if (obj.optInt("statusCode") == 200 && obj.has("data") + && obj.getJSONObject("data").has("refreshToken")) { + saveToken(obj.getJSONObject("data").getString("refreshToken")); + connectedUser = userName.trim(); + rejectedUser = null; + deleteRejectedUser(); + state = State.CONNECTED; + lastError = null; + logger.info("Central token captured for user {}", connectedUser); + return true; + } + // wrong username/password or account problem: do not retry with these credentials + lastError = obj.optString("errorMessage", "Central login rejected"); + rejectedUser = userName.trim(); + saveRejectedUser(rejectedUser); + state = State.PASSWORD_MISMATCH; + logger.warn("Central login rejected for user {}: {}", userName, lastError); + } catch (Exception e) { + // network problem: nothing saved, next login tries again + lastError = "Central server not reachable: " + e.getMessage(); + logger.warn("Central login not possible right now: {}", e.getMessage()); + } + return false; + } + + /** + * Returns a fresh central session key (refreshToken -> JWT -> session key), + * or null if this laptop has no usable token. Throws on network errors so + * callers can retry later. + */ + public synchronized String getCentralSessionKey() throws Exception { + if (!isEnabled() || refreshToken == null) { + return null; + } + String jwt; + long start = System.currentTimeMillis(); + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + JSONObject body = new JSONObject(); + body.put("refreshToken", refreshToken); + ResponseEntity response = restTemplate.exchange(centralRefreshTokenUrl, HttpMethod.POST, + new HttpEntity<>(body.toString(), headers), String.class); + JSONObject obj = new JSONObject(response.getBody()); + jwt = obj.getString("jwtToken"); + if (obj.has("refreshToken")) { + saveToken(obj.getString("refreshToken")); + } + lastRefreshAt = System.currentTimeMillis(); + logger.info("Central token refreshed in {} ms", lastRefreshAt - start); + } catch (HttpStatusCodeException e) { + if (e.getStatusCode().value() == 401) { + // expired, revoked or user deactivated on central + logger.warn("Central refresh token rejected ({}); a new login is needed", + e.getResponseBodyAsString()); + deleteToken(); + state = State.RECONNECT_NEEDED; + lastError = "Central connection expired, please log in again"; + return null; + } + throw e; + } + + // JWT -> central session key; Common-API creates a new Redis session for it + long sessionStart = System.currentTimeMillis(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Jwttoken", jwt); + ResponseEntity response = restTemplate.exchange(centralSessionUrl, HttpMethod.POST, + new HttpEntity<>("{}", headers), String.class); + JSONObject obj = new JSONObject(response.getBody()); + if (obj.optInt("statusCode") != 200 || !obj.has("data")) { + throw new IllegalStateException("Central session not created: " + obj.optString("errorMessage")); + } + Object data = obj.get("data"); + JSONObject dataObj = data instanceof JSONObject ? (JSONObject) data : new JSONObject(data.toString()); + state = State.CONNECTED; + lastError = null; + logger.info("Central session created in {} ms", System.currentTimeMillis() - sessionStart); + return dataObj.getString("key"); + } + + /** Keeps the 90-day token alive even when no IDs are needed. */ + public void keepAliveIfDue(long maxAgeMs) { + if (refreshToken != null && System.currentTimeMillis() - lastRefreshAt > maxAgeMs) { + try { + getCentralSessionKey(); + } catch (Exception e) { + logger.debug("Token keep-alive skipped: {}", e.getMessage()); + } + } + } + + // ---------------------------------------------------------------- token file + + private Path tokenPath() { + return Paths.get(tokenDir, TOKEN_FILE_NAME); + } + + private void saveToken(String token) { + refreshToken = token; + try { + Path dir = Paths.get(tokenDir); + Files.createDirectories(dir); + setPermissions(dir, "rwx------"); + Path tmp = dir.resolve(TOKEN_FILE_NAME + ".tmp"); + Files.write(tmp, encrypt(token).getBytes(StandardCharsets.UTF_8)); + setPermissions(tmp, "rw-------"); + try { + Files.move(tmp, tokenPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp, tokenPath(), StandardCopyOption.REPLACE_EXISTING); + } + } catch (Exception e) { + // keep working from memory; the token is captured again after a restart + logger.warn("Could not save central token to {} (kept in memory only): {}", tokenDir, e.getMessage()); + } + } + + private String readTokenFile() { + try { + Path path = tokenPath(); + if (Files.exists(path)) { + return decrypt(new String(Files.readAllBytes(path), StandardCharsets.UTF_8).trim()); + } + } catch (Exception e) { + logger.warn("Stored central token could not be read, a new login is needed: {}", e.getMessage()); + } + return null; + } + + private void deleteToken() { + refreshToken = null; + try { + Files.deleteIfExists(tokenPath()); + } catch (Exception e) { + logger.warn("Could not delete central token file: {}", e.getMessage()); + } + } + + private String readRejectedUser() { + try { + Path path = Paths.get(tokenDir, REJECTED_FILE_NAME); + if (Files.exists(path)) { + String user = new String(Files.readAllBytes(path), StandardCharsets.UTF_8).trim(); + return user.isEmpty() ? null : user; + } + } catch (Exception e) { + logger.warn("Could not read rejected-login marker: {}", e.getMessage()); + } + return null; + } + + private void saveRejectedUser(String user) { + try { + Path dir = Paths.get(tokenDir); + Files.createDirectories(dir); + Path path = dir.resolve(REJECTED_FILE_NAME); + Files.write(path, user.getBytes(StandardCharsets.UTF_8)); + setPermissions(path, "rw-------"); + } catch (Exception e) { + logger.warn("Could not save rejected-login marker: {}", e.getMessage()); + } + } + + private void deleteRejectedUser() { + try { + Files.deleteIfExists(Paths.get(tokenDir, REJECTED_FILE_NAME)); + } catch (Exception e) { + logger.warn("Could not delete rejected-login marker: {}", e.getMessage()); + } + } + + private static void setPermissions(Path path, String perms) { + try { + Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(perms)); + } catch (Exception e) { + // non-POSIX file system; nothing more we can do + } + } + + private SecretKeySpec machineKey() throws Exception { + String machineId; + try { + machineId = new String(Files.readAllBytes(Paths.get("/etc/machine-id")), StandardCharsets.UTF_8).trim(); + } catch (Exception e) { + machineId = InetAddress.getLocalHost().getHostName(); + } + byte[] key = MessageDigest.getInstance("SHA-256") + .digest((machineId + ":" + tokenSecret).getBytes(StandardCharsets.UTF_8)); + return new SecretKeySpec(key, "AES"); + } + + private String encrypt(String plain) throws Exception { + byte[] iv = new byte[12]; + new SecureRandom().nextBytes(iv); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, machineKey(), new GCMParameterSpec(128, iv)); + byte[] enc = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8)); + byte[] out = new byte[iv.length + enc.length]; + System.arraycopy(iv, 0, out, 0, iv.length); + System.arraycopy(enc, 0, out, iv.length, enc.length); + return Base64.getEncoder().encodeToString(out); + } + + private String decrypt(String encoded) throws Exception { + byte[] in = Base64.getDecoder().decode(encoded); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, machineKey(), new GCMParameterSpec(128, in, 0, 12)); + return new String(cipher.doFinal(in, 12, in.length - 12), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncService.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncService.java new file mode 100644 index 00000000..e8971fe7 --- /dev/null +++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncService.java @@ -0,0 +1,239 @@ +/* +* 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.mmu.service.dataSyncActivity; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.locks.ReentrantLock; + +import org.json.JSONArray; +import org.json.JSONObject; +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.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import com.iemr.mmu.service.benid.BenIdAutoTopUpService; +import com.iemr.mmu.service.benid.CentralAuthService; + +import jakarta.annotation.PreDestroy; + +/** + * StopTB laptops: runs Data Sync by itself, so central stays current and laptops get master changes without + * anyone pressing Upload or Download Masters. + * + * - Upload (van to central) every sync.autoUpload.intervalMs (default 30 min). + * - Master download (central to van) every sync.autoMasterDownload.intervalMs (default 6 h). + * + * Uses the central connection of the automatic BenID download (CentralAuthService): no password is stored, and + * SyncedBy is the operator who connected the laptop. Active only where CentralAuthService is enabled + * (stoptb.enforce.vanid=true and stoptb.van.id>0); central and other deployments are unaffected. The manual + * buttons keep working as before. + * + * Off by default (manual sync only) until the automatic schedule is approved: set sync.autoUpload.enabled, + * sync.autoMasterDownload.enabled and sync.autoInventoryDownload.enabled to true to switch it on. + */ +@Service +public class AutoSyncService { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + /** Upload and master download never run at the same time from here. */ + private static final ReentrantLock SYNC_LOCK = new ReentrantLock(); + + @Autowired + private CentralAuthService centralAuthService; + + @Autowired + private UploadDataToServerImpl uploadDataToServerImpl; + + @Autowired + private DownloadDataFromServerImpl downloadDataFromServerImpl; + + @Autowired + private DownloadDataFromServerTransactionalImpl downloadDataFromServerTransactional; + + @Autowired + private DataSyncRepository dataSyncRepository; + + @Autowired + private BenIdAutoTopUpService benIdAutoTopUpService; + + @Value("${sync.autoUpload.enabled:false}") + private boolean uploadEnabled; + + @Value("${sync.autoMasterDownload.enabled:false}") + private boolean masterDownloadEnabled; + + // refills / stock issued at central for this van's store ("Inventory Data Download"), after each upload + @Value("${sync.autoInventoryDownload.enabled:false}") + private boolean inventoryDownloadEnabled; + + // automatic master download fetches only the inventory masters (items, stores, item->store mapping, Min Stock); + // other masters (users, complaints, villages) come with the manual Download Masters button + @Value("${sync.autoMasterDownload.inventoryOnly:true}") + private boolean masterDownloadInventoryOnly; + + static final Set INVENTORY_MASTERS = Set.of("m_item", "m_itemcategory", "m_itemissueconfig", "m_uom", + "m_manufacturer", "m_pharmacologicalcategory", "m_supplier", "m_facilitytype", "m_facility", + "m_itemfacilitymapping"); + + private volatile long lastUploadAt; + private volatile String lastUploadResult; + private volatile long lastMasterDownloadAt; + private volatile String lastMasterDownloadResult; + private volatile long lastInventoryDownloadAt; + private volatile String lastInventoryDownloadResult; + private volatile String lastError; + + // own thread: a long upload must not hold the shared scheduler thread + private final ExecutorService background = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "auto-data-sync"); + t.setDaemon(true); + return t; + }); + + @PreDestroy + public void shutdown() { + background.shutdownNow(); + } + + @Scheduled(initialDelayString = "${sync.autoUpload.initialDelayMs:600000}", fixedDelayString = "${sync.autoUpload.intervalMs:1800000}") + public void scheduledUpload() { + if (uploadEnabled && centralAuthService.isEnabled()) { + background.submit(this::uploadOnce); + } + } + + @Scheduled(initialDelayString = "${sync.autoMasterDownload.initialDelayMs:900000}", fixedDelayString = "${sync.autoMasterDownload.intervalMs:21600000}") + public void scheduledMasterDownload() { + if (masterDownloadEnabled && centralAuthService.isEnabled()) { + background.submit(this::masterDownloadOnce); + } + } + + void uploadOnce() { + if (!SYNC_LOCK.tryLock()) { + return; + } + try { + String key = centralAuthService.getCentralSessionKey(); + if (key == null) { + lastError = "No central connection - waiting for a user login"; + return; + } + int vanID = centralAuthService.getVanID(); + String user = centralAuthService.getConnectedUser(); + String result = uploadDataToServerImpl.getDataToSyncToServer(vanID, + user != null && !user.isBlank() ? user : "auto-sync", key, null); + lastUploadAt = System.currentTimeMillis(); + lastUploadResult = result; + lastError = null; + logger.info("Automatic upload for vanID {} finished: {}", vanID, result); + // internet is up: top up the BenID pool if it is low (same as the manual upload) + benIdAutoTopUpService.triggerNow(); + if (inventoryDownloadEnabled) + inventoryDownload(vanID, key); + } catch (Exception e) { + lastError = "Upload: " + e.getMessage(); + logger.warn("Automatic upload failed: {}", e.getMessage()); + } finally { + SYNC_LOCK.unlock(); + } + } + + // Same as the "Inventory Data Download" button: indents, indent issues, stock transfers and stock entries + // that central has for this van's store. Its own failure does not undo the upload. + private void inventoryDownload(int vanID, String key) { + try { + int done = downloadDataFromServerTransactional.downloadTransactionalData(vanID, key, "datasync"); + lastInventoryDownloadAt = System.currentTimeMillis(); + lastInventoryDownloadResult = done > 0 ? "success" : "nothing downloaded"; + logger.info("Automatic inventory download for vanID {} finished: {}", vanID, lastInventoryDownloadResult); + } catch (Exception e) { + lastError = "Inventory download: " + e.getMessage(); + logger.warn("Automatic inventory download failed: {}", e.getMessage()); + } + } + + void masterDownloadOnce() { + if (!SYNC_LOCK.tryLock()) { + return; + } + try { + String key = centralAuthService.getCentralSessionKey(); + if (key == null) { + lastError = "No central connection - waiting for a user login"; + return; + } + int vanID = centralAuthService.getVanID(); + Integer psm = dataSyncRepository.getVanProviderServiceMapID(vanID); + if (psm == null) { + lastError = "Master download: van " + vanID + " not found in m_van"; + return; + } + String result = downloadDataFromServerImpl.downloadMasterDataFromServer(key, null, vanID, psm, + masterDownloadInventoryOnly ? INVENTORY_MASTERS : null); + if ("inProgress".equalsIgnoreCase(result)) { + logger.info("Master download already running (manual) - skipping the automatic one"); + return; + } + lastMasterDownloadAt = System.currentTimeMillis(); + lastMasterDownloadResult = result; + lastError = null; + logger.info("Automatic master download for vanID {} finished: {}", vanID, result); + } catch (Exception e) { + lastError = "Master download: " + e.getMessage(); + logger.warn("Automatic master download failed: {}", e.getMessage()); + } finally { + SYNC_LOCK.unlock(); + } + } + + /** For the Data Sync screen: last runs and the rows central has rejected. */ + public JSONObject status() { + JSONObject s = new JSONObject(); + s.put("enabled", centralAuthService.isEnabled()); + s.put("uploadEnabled", uploadEnabled); + s.put("masterDownloadEnabled", masterDownloadEnabled); + s.put("lastUploadAt", lastUploadAt); + s.put("lastUploadResult", lastUploadResult == null ? JSONObject.NULL : lastUploadResult); + s.put("lastMasterDownloadAt", lastMasterDownloadAt); + s.put("lastMasterDownloadResult", + lastMasterDownloadResult == null ? JSONObject.NULL : lastMasterDownloadResult); + s.put("inventoryDownloadEnabled", inventoryDownloadEnabled); + s.put("lastInventoryDownloadAt", lastInventoryDownloadAt); + s.put("lastInventoryDownloadResult", + lastInventoryDownloadResult == null ? JSONObject.NULL : lastInventoryDownloadResult); + s.put("lastError", lastError == null ? JSONObject.NULL : lastError); + List> failures = dataSyncRepository.getSyncFailures(); + long failedRows = failures.stream().mapToLong(f -> ((Number) f.get("failedRows")).longValue()).sum(); + s.put("failedRows", failedRows); + s.put("failures", new JSONArray(failures)); + return s; + } +} diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DataSyncRepository.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DataSyncRepository.java index 75a74842..99b4ec04 100644 --- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DataSyncRepository.java +++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DataSyncRepository.java @@ -23,9 +23,11 @@ import java.sql.Timestamp; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.sql.PreparedStatement; @@ -40,6 +42,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Service; +import com.iemr.mmu.data.syncActivity_syncLayer.SyncUtilityClass; import com.iemr.mmu.repo.syncActivity_syncLayer.SyncUtilityClassRepo; import com.iemr.mmu.utils.validator.SqlIdentifierValidator; @@ -127,9 +130,103 @@ public int updateProcessedFlagInVan(String schemaName, String tableName, List psm = getJdbcTemplate().queryForList( + "SELECT ProviderServiceMapID FROM db_iemr.m_van WHERE VanID = ? AND Deleted = false", Integer.class, + vanID); + return psm.isEmpty() ? null : psm.get(0); + } + + /** + * Rows central rejected on upload (Processed = 'F'), per sync table and reason. They are sent again on every + * upload until central accepts them. + */ + public List> getSyncFailures() { + jdbcTemplate = getJdbcTemplate(); + List> result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (SyncUtilityClass table : syncutilityClassRepo.findByDeletedOrderBySyncTableDetailID(false)) { + if (Boolean.TRUE.equals(table.getIsMaster()) || table.getSchemaName() == null + || table.getTableName() == null) + continue; + String qualified = SqlIdentifierValidator.validatedSchemaName(table.getSchemaName()) + "." + + SqlIdentifierValidator.validatedTableName(table.getTableName()); + if (!seen.add(qualified.toLowerCase())) + continue; + try { + List> rows = jdbcTemplate.queryForList("SELECT '" + table.getTableName() + + "' AS tableName, SyncFailureReason AS reason, COUNT(*) AS failedRows, DATE_FORMAT(MAX(SyncedDate), '%d-%m-%Y %H:%i') AS lastTried " + + "FROM " + qualified + " WHERE Processed = 'F' GROUP BY SyncFailureReason"); + result.addAll(rows); + } catch (Exception e) { + logger.warn("Sync failure check skipped for {}: {}", qualified, e.getMessage()); + } + } + return result; + } + // ---------------------------------- End of Upload repository // ---------------------------------- Download Repository + + /** + * Down-sync copies the beneficiary tables but not i_ben_flow_outreach, and the tab download + * (FLW-API getBeneficiaryData) only serves beneficiaries that have a worklist row. So a + * beneficiary registered elsewhere and down-synced here never reached this laptop's tabs. + * Creates the missing worklist row for this van's beneficiaries, filled as TM-API does at + * registration. Processed = 'P' so it is not uploaded back: central already has its own row. + */ + public int createMissingWorklistRows(int vanID, int providerServiceMapID) { + jdbcTemplate = getJdbcTemplate(); + + String village = "(SELECT h.%s FROM db_identity.i_beneficiarydetails_rmnch r " + + " JOIN db_identity.i_householddetails h ON h.houseoldId = r.houseoldId " + + " WHERE r.BeneficiaryRegID = bm.BenRegId LIMIT 1)"; + + String insert = "INSERT INTO db_iemr.i_ben_flow_outreach " + + " (beneficiary_reg_id, beneficiary_id, visit_no, ben_name, ben_age, ben_dob, ben_gender, " + + " ben_gender_val, ben_age_val, ben_phone_no, father_name, spouse_name, nurse_flag, doctor_flag, " + + " pharmacist_flag, doctor_signature_flag, BenArrivedFlag, created_by, created_date, modified_by, " + + " modified_date, registrationDate, districtID, district, villageID, village, " + + " providerServiceMapID, vanID, Processed, deleted) " + + " SELECT * FROM ( SELECT bm.BenRegId, rg.BeneficiaryID, 1 AS visit_no, " + + " TRIM(CONCAT_WS(' ', d.FirstName, NULLIF(d.MiddleName, ''), d.LastName)) AS ben_name, " + + " CONCAT(TIMESTAMPDIFF(YEAR, d.DOB, bm.CreatedDate), ' years - ', " + + " MOD(TIMESTAMPDIFF(MONTH, d.DOB, bm.CreatedDate), 12), ' months') AS ben_age, " + + " d.DOB, d.Gender, d.GenderId, TIMESTAMPDIFF(YEAR, d.DOB, bm.CreatedDate) AS ben_age_val, " + + " COALESCE(c.PreferredPhoneNum, c.PhoneNum1) AS phone, " + + " COALESCE(d.FatherName, '') AS father, COALESCE(d.SpouseName, '') AS spouse, " + + " 1 AS nf, 0 AS df, 0 AS pf, 0 AS dsf, 0 AS baf, bm.CreatedBy, bm.CreatedDate AS cdate, " + + " 'downsync' AS mby, NOW() AS mdate, bm.CreatedDate AS rdate, " + + " COALESCE(a.CurrDistrictId, a.PermDistrictId) AS districtID, " + + " COALESCE(a.CurrDistrict, a.PermDistrict) AS district, " + + " COALESCE(" + String.format(village, "villageid") + ", a.CurrVillageId, a.PermVillageId) AS villageID, " + + " COALESCE(" + String.format(village, "village") + ", a.CurrVillage, a.PermVillage) AS village, " + + " ? AS psm, bm.VanID, 'P' AS processed, 0 AS deleted " + + " FROM db_identity.i_beneficiarymapping bm " + + " JOIN db_identity.m_beneficiaryregidmapping rg ON rg.BenRegId = bm.BenRegId " + + " JOIN db_identity.i_beneficiarydetails d ON d.BeneficiaryDetailsId = bm.BenDetailsId " + + " LEFT JOIN db_identity.i_beneficiarycontacts c ON c.BenContactsID = bm.BenContactsId " + + " LEFT JOIN db_identity.i_beneficiaryaddress a ON a.BenAddressID = bm.BenAddressId " + + " WHERE bm.VanID = ? AND bm.Deleted = false " + // the van can hold beneficiaries of other service lines; only its own get a row + + " AND (SELECT r.ProviderServiceMapID FROM db_identity.i_beneficiarydetails_rmnch r " + + " WHERE r.BeneficiaryRegID = bm.BenRegId LIMIT 1) = ? " + + " AND NOT EXISTS (SELECT 1 FROM db_iemr.i_ben_flow_outreach f " + + " WHERE f.beneficiary_reg_id = bm.BenRegId) ) x " + + " WHERE x.villageID IS NOT NULL"; + + int inserted = jdbcTemplate.update(insert, providerServiceMapID, vanID, providerServiceMapID); + + if (inserted > 0) + // same convention as TM-API: VanSerialNo is the row's own id + jdbcTemplate.update("UPDATE db_iemr.i_ben_flow_outreach SET VanSerialNo = ben_flow_id " + + " WHERE modified_by = 'downsync' AND VanSerialNo IS NULL"); + + return inserted; + } + public int[] updateLatestMasterInLocal(String query, List syncDataList) { int[] i = null; // get JDBC template @@ -176,6 +273,17 @@ public String resolveLastModColumn(String schema, String table) { return (found == null || found.isEmpty()) ? null : found.get(0); } + public Set getTableColumns(String schema, String table) { + jdbcTemplate = getJdbcTemplate(); + + String query = " SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? "; + + Set columns = new HashSet<>(); + for (String c : jdbcTemplate.queryForList(query, String.class, schema, table)) + columns.add(c.toLowerCase()); + return columns; + } + public List getDownSyncColumns(String schema, String table) { jdbcTemplate = getJdbcTemplate(); diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownSyncDataFromServerImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownSyncDataFromServerImpl.java index c3c1e1d0..2c70854e 100644 --- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownSyncDataFromServerImpl.java +++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownSyncDataFromServerImpl.java @@ -163,6 +163,7 @@ public String startDownSync(String serverAuthorization, String jwtToken, Integer } progressCounter++; } + createMissingWorklistRows(vanID, providerServiceMapID); countOutstandingConflicts(downSyncTables, vanID); } finally { IN_PROGRESS.set(false); @@ -207,6 +208,23 @@ public Map getDownSyncStatus() { return resultMap; } + /** Down-synced beneficiaries need a worklist row before this laptop's tabs can download them. */ + private void createMissingWorklistRows(Integer vanID, Integer providerServiceMapID) { + try { + Integer psm = providerServiceMapID != null ? providerServiceMapID + : dataSyncRepository.getVanProviderServiceMapID(vanID); + if (psm == null) { + logger.warn("Worklist rows not created after down-sync: no service line for van {}", vanID); + return; + } + int created = dataSyncRepository.createMissingWorklistRows(vanID, psm); + logger.info("Created {} worklist rows for down-synced beneficiaries of van {}", created, vanID); + } catch (Exception e) { + // the down-sync itself already succeeded; a failure here must not undo it + logger.error("Creating worklist rows after down-sync failed for van " + vanID + " : " + e.getMessage(), e); + } + } + private void countOutstandingConflicts(List downSyncTables, Integer vanID) { outstandingConflictCounter = 0; outstandingConflictTables.clear(); diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImpl.java index 6a6d12bc..572724be 100644 --- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImpl.java +++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImpl.java @@ -27,9 +27,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import org.json.JSONObject; import org.slf4j.Logger; @@ -45,6 +47,7 @@ import com.google.gson.Gson; import com.iemr.mmu.data.syncActivity_syncLayer.MasterDownloadDataDigester; +import com.iemr.mmu.service.benid.BenIdAutoTopUpService; import com.iemr.mmu.data.syncActivity_syncLayer.SyncDownloadMaster; import com.iemr.mmu.data.syncActivity_syncLayer.TempVan; import com.iemr.mmu.repo.syncActivity_syncLayer.SyncDownloadMasterRepo; @@ -60,6 +63,9 @@ public class DownloadDataFromServerImpl implements DownloadDataFromServer { // rest URLs from server to download master data for van @Value("${dataSyncDownloadUrl}") private String dataSyncDownloadUrl; + // StopTB camp laptop (laptops are set up from copied databases); other MMU deployments keep standard behaviour + @Value("${stoptb.enforce.vanid:false}") + private boolean stopTBLaptop; @Autowired private SyncDownloadMasterRepo syncDownloadMasterRepo; @Autowired @@ -95,6 +101,14 @@ public class DownloadDataFromServerImpl implements DownloadDataFromServer { */ public String downloadMasterDataFromServer(String ServerAuthorization, String jwtToken, Integer vanID, Integer psmID) throws Exception { + return downloadMasterDataFromServer(ServerAuthorization, jwtToken, vanID, psmID, null); + } + + /** + * @param onlyTables master tables to download (case-insensitive), or null for all of m_downloadmastertable + */ + public String downloadMasterDataFromServer(String ServerAuthorization, String jwtToken, Integer vanID, Integer psmID, + Set onlyTables) throws Exception { if (totalCounter != progressCounter) { return "inProgress"; @@ -103,6 +117,10 @@ public String downloadMasterDataFromServer(String ServerAuthorization, String jw String successFlag = " Master download started "; ArrayList downloadMasterList = syncDownloadMasterRepo.getDownloadTables(); + if (onlyTables != null && downloadMasterList != null) { + downloadMasterList.removeIf(t -> t.getTableName() == null + || !onlyTables.contains(t.getTableName().toLowerCase())); + } totalCounter = downloadMasterList.size(); progressCounter = 0; @@ -178,6 +196,8 @@ public String call() { private int downloadDataFromServer(SyncDownloadMaster syncDownloadMaster,String ServerAuthorization, String jwtToken) throws Exception { int successFlag = 0; + if (stopTBLaptop) + fullDownloadForLineAndVanMasters(syncDownloadMaster); // initializing RestTemplate RestTemplate restTemplate = new RestTemplate(); // Provide the required second argument, e.g., an empty string or appropriate authorization token @@ -210,6 +230,18 @@ private int downloadDataFromServer(SyncDownloadMaster syncDownloadMaster,String return successFlag; } + /** + * StopTB laptops only. Service-line (P) and van (V) masters - hubs, vans, service points, stores, items, item-store mapping, roles - + * are always downloaded in full for this van's line: they are small, and a laptop set up from a copied database + * can carry download dates newer than central rows it never received (missing hub -> its vans fail on the + * foreign key). Rows are inserted/updated by ID, so repeating them is harmless. Masters shared by all lines (A, + * e.g. 661k villages) stay incremental. + */ + static void fullDownloadForLineAndVanMasters(SyncDownloadMaster table) { + if ("P".equalsIgnoreCase(table.getMasterType()) || "V".equalsIgnoreCase(table.getMasterType())) + table.setLastDownloadDate(null); + } + private int updateMastersWithLatestData(MasterDownloadDataDigester masterDownloadDataDigester, SyncDownloadMaster syncDownloadMaster) { int successFlag = 0; @@ -326,6 +358,10 @@ public Map getDownloadStatus() { public int callCentralAPIToGenerateBenIDAndimportToLocal(String requestOBJ, String Authorization, String ServerAuthorization, String token) throws Exception { int i = 0, i1 = 0; + // never run together with the automatic top-up, so the same IDs are not requested twice + if (!BenIdAutoTopUpService.GENERATION_LOCK.tryLock(2, TimeUnit.MINUTES)) { + throw new Exception("BenID generation is already running, please try again in a few minutes"); + } try{ JSONObject originalRequest = new JSONObject(requestOBJ); BigInteger vanID = null; @@ -378,6 +414,8 @@ public int callCentralAPIToGenerateBenIDAndimportToLocal(String requestOBJ, Stri } catch (Exception e) { logger.error("Error while generating catch UNIQUE_ID at central server: " + e.getMessage()); throw new Exception("Error while generating catch UNIQUE_ID at central server: " + e.getMessage()); + } finally { + BenIdAutoTopUpService.GENERATION_LOCK.unlock(); } return i; } diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImpl.java index ee3c39a5..e441ea6e 100644 --- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImpl.java +++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImpl.java @@ -227,8 +227,6 @@ public int downloadTransactionalData(int vanID, String ServerAuthorization, Stri default: } - - i++; } return 1; diff --git a/src/main/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImpl.java index d9567861..b0aa115d 100644 --- a/src/main/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImpl.java +++ b/src/main/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImpl.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.json.JSONArray; import org.json.JSONException; @@ -80,6 +81,11 @@ public class UploadDataToServerImpl implements UploadDataToServer { @Value("${BATCH_SIZE}") private int BATCH_SIZE; + // StopTB camp laptop: a table configured in several sync groups is uploaded only by its last group (see + // skipTablesUploadedByLaterGroup) + @Value("${stoptb.enforce.vanid:false}") + private boolean stopTBLaptop; + @Autowired private DataSyncRepository dataSyncRepository; @Autowired @@ -151,9 +157,33 @@ private String startDataSync(int vanID, String user, String Authorization, Strin String tableKey = obj.getSchemaName() + "." + obj.getTableName(); boolean tableHasError = false; - // get data from DB to sync to server - List> syncData = getDataToSync(obj.getSchemaName(), obj.getTableName(), - obj.getVanColumnName()); + String[] columns = { obj.getVanColumnName(), obj.getServerColumnName() }; + List> syncData; + try { + if (stopTBLaptop) + columns = useRenamedColumns(obj.getTableName(), columns[0], columns[1], + dataSyncRepository.getTableColumns(obj.getSchemaName(), obj.getTableName())); + + // get data from DB to sync to server + syncData = getDataToSync(obj.getSchemaName(), obj.getTableName(), columns[0]); + } catch (Exception e) { + if (!stopTBLaptop) + throw e; + // StopTB camp laptop: one table that cannot be read must not stop the upload of the others; + // its rows stay unprocessed and go on the next upload + logger.error("Upload of table {} skipped: {}", tableKey, e.getMessage()); + groupHasFailures = true; + Map tableDetails = new HashMap<>(); + tableDetails.put("tableName", obj.getTableName()); + tableDetails.put("schemaName", obj.getSchemaName()); + tableDetails.put("status", "failed"); + tableDetails.put("totalRecords", 0); + tableDetails.put("successfulRecords", 0); + tableDetails.put("failedRecords", 0); + tableDetails.put("failureReasons", List.of("Table could not be read: " + e.getMessage())); + tableDetailsList.add(tableDetails); + continue; + } logger.info("Fetched syncData for schema {} and table {}: {}", obj.getSchemaName(), obj.getTableName(), objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(syncData)); @@ -179,7 +209,7 @@ private String startDataSync(int vanID, String user, String Authorization, Strin Map syncResult = syncDataToServer(vanID, obj.getSchemaName(), obj.getTableName(), - obj.getVanAutoIncColumnName(), obj.getServerColumnName(), syncDataBatch, user, + obj.getVanAutoIncColumnName(), columns[1], syncDataBatch, user, Authorization, token); if (syncResult == null) { @@ -221,7 +251,7 @@ private String startDataSync(int vanID, String user, String Authorization, Strin Map syncResult = syncDataToServer(vanID, obj.getSchemaName(), obj.getTableName(), - obj.getVanAutoIncColumnName(), obj.getServerColumnName(), syncDataBatch, user, + obj.getVanAutoIncColumnName(), columns[1], syncDataBatch, user, Authorization, token); if (syncResult == null) { @@ -407,10 +437,59 @@ private String startDataSync(int vanID, String user, String Authorization, Strin private List getVanAndServerColumns(Integer groupID) throws Exception { List syncUtilityClassList = getVanAndServerColumnList(groupID); + if (stopTBLaptop) + syncUtilityClassList = skipTablesUploadedByLaterGroup(syncUtilityClassList, groupID, + syncutilityClassRepo.findByDeletedOrderBySyncTableDetailID(false)); logger.debug("Fetched SyncUtilityClass list for groupID {}: {}", groupID, syncUtilityClassList); return syncUtilityClassList; } + /** + * Inventory tables are configured twice: group 5 (old lists without the store columns of stock-outs and patient + * issues, batch insert fails) and group 9 "Inventory" (complete). Group 5 ran first and marked the rows uploaded, + * so central got stock-outs without their camp store. A table that a later group also uploads is skipped here. + */ + static List skipTablesUploadedByLaterGroup(List groupTables, int groupID, + List allTables) { + Map lastGroup = new HashMap<>(); + for (SyncUtilityClass t : allTables) { + if (t.getTableName() != null && t.getSyncTableGroupID() != null) + lastGroup.merge(t.getTableName().toLowerCase(), t.getSyncTableGroupID(), Math::max); + } + List kept = new ArrayList<>(); + for (SyncUtilityClass t : groupTables) { + Integer last = t.getTableName() == null ? null : lastGroup.get(t.getTableName().toLowerCase()); + if (last == null || last <= groupID) + kept.add(t); + } + return kept; + } + + // columns renamed by DB scripts after the sync table lists were written (tb_diagnostic_order: V114 renamed + // reason_for_refusal to reason_to_close, V117 reverted it, V125 renamed it again) + private static final Map> RENAMED_COLUMNS = Map.of("tb_diagnostic_order", + Map.of("reason_for_refusal", "reason_to_close", "reason_to_close", "reason_for_refusal")); + + /** + * The sync table list may still name a column by its old name (laptop m_synctabledetail has reason_for_refusal, + * the table has reason_to_close) and the whole upload stopped with "bad SQL grammar". The name the table really + * has is used in both the van and the server column lists (central has the same DB version). + */ + static String[] useRenamedColumns(String tableName, String vanColumns, String serverColumns, + Set tableColumns) { + Map renamed = tableName == null ? null : RENAMED_COLUMNS.get(tableName.toLowerCase()); + if (renamed == null || tableColumns == null || tableColumns.isEmpty()) + return new String[] { vanColumns, serverColumns }; + for (Map.Entry r : renamed.entrySet()) { + if (!tableColumns.contains(r.getKey()) && tableColumns.contains(r.getValue())) { + String listed = "(?i)\\b" + r.getKey() + "\\b"; + vanColumns = vanColumns == null ? null : vanColumns.replaceAll(listed, r.getValue()); + serverColumns = serverColumns == null ? null : serverColumns.replaceAll(listed, r.getValue()); + } + } + return new String[] { vanColumns, serverColumns }; + } + public List getVanAndServerColumnList(Integer groupID) throws Exception { List syncUtilityClassList = syncutilityClassRepo .findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(groupID, false); diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentral.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentral.java index 0da5bf4b..75aa52a4 100644 --- a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentral.java +++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentral.java @@ -173,6 +173,22 @@ public int checkRecordIsAlreadyPresentOrNot(String schemaName, String tableName, } } + /** + * Central ID of a row a van uploaded earlier, found by the van's own ID (VanSerialNo) and the van; null if that + * row is not on central (yet). + */ + public Long findCentralIdOfVanRow(String schemaName, String tableName, String idColumn, Object vanSerialNo, + String vanID) { + if (!isValidSchemaName(schemaName) || !isValidDatabaseIdentifierCharacter(tableName) + || !isValidDatabaseIdentifierCharacter(idColumn)) { + throw new IllegalArgumentException("Invalid identifiers provided."); + } + List ids = getJdbcTemplate().queryForList("SELECT " + idColumn + " FROM " + schemaName + "." + tableName + + " WHERE VanSerialNo = ? AND VanID = ? ORDER BY " + idColumn + " DESC LIMIT 1", Long.class, + String.valueOf(vanSerialNo), vanID); + return ids.isEmpty() ? null : ids.get(0); + } + public int[] syncDataToCentralDB(String schema, String tableName, String serverColumns, String query, List syncDataList) { jdbcTemplate = getJdbcTemplate(); diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralDownload.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralDownload.java index c76962b4..50c49c0e 100644 --- a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralDownload.java +++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralDownload.java @@ -205,7 +205,8 @@ else if (masterType.equalsIgnoreCase("P")) { } logger.info("Select query central: " + baseQuery); logger.info("Last Downloaded Date " + lastDownloadDate); - logger.info("Result set Details: " + resultSetList); + // Row count only: logging the rows of a large master (villages, 661k rows) exhausted the heap. + logger.info("Rows returned: " + resultSetList.size()); return resultSetList; } diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentFetchService.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentFetchService.java new file mode 100644 index 00000000..a03526f9 --- /dev/null +++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentFetchService.java @@ -0,0 +1,62 @@ +package com.iemr.mmu.service.dataSyncLayerCentral; + +import java.time.Duration; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; + +/*** + * @purpose Hands back a short-lived presigned URL for the latest successfully-pushed + * diagnostic document matching a beneficiary+documentType, generated on demand from + * the object's S3 key (s3_path) - the bucket is private, so no permanent URL is + * ever persisted or handed out. + */ +@Service +public class DiagnosticDocumentFetchService { + + private static final Duration URL_VALIDITY = Duration.ofMinutes(15); + + @Value("${diagnostic.documents.s3.bucket}") + private String bucket; + + @Autowired + private DiagnosticDocumentRepository diagnosticDocumentRepository; + + @Autowired + private S3Presigner s3Presigner; + + /*** + * @return null if no successfully-pushed document matches, otherwise the download details + * (documentType, orderType, externalOrderId, contentType, lastModDate, downloadUrl, + * urlExpiresInSeconds) + */ + public Map getLatestDocumentDownload(Long beneficiaryId, String documentType) { + Map row = diagnosticDocumentRepository.findLatestDocument(beneficiaryId, documentType); + if (row == null) { + return null; + } + + String s3Key = (String) row.get("s3_path"); + GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(bucket).key(s3Key).build(); + GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder().signatureDuration(URL_VALIDITY) + .getObjectRequest(getObjectRequest).build(); + String downloadUrl = s3Presigner.presignGetObject(presignRequest).url().toString(); + + Map result = new java.util.HashMap<>(); + result.put("externalOrderId", row.get("external_order_id")); + result.put("orderType", row.get("order_type")); + result.put("documentType", row.get("document_type")); + result.put("contentType", row.get("content_type")); + result.put("originalFileName", row.get("original_file_name")); + result.put("lastModDate", String.valueOf(row.get("last_mod_date"))); + result.put("downloadUrl", downloadUrl); + result.put("urlExpiresInSeconds", URL_VALIDITY.getSeconds()); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentIngestService.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentIngestService.java new file mode 100644 index 00000000..d04d841f --- /dev/null +++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentIngestService.java @@ -0,0 +1,139 @@ +package com.iemr.mmu.service.dataSyncLayerCentral; + +import java.lang.reflect.Type; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +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.stereotype.Service; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.ServerSideEncryption; + +/*** + * @purpose Receives a batch of decrypted diagnostic documents pushed from a van and stores + * each in S3 - purely a storage relay, no database writes here. The pushing van + * persists its own record locally (DiagnosticDocumentPushServiceImpl.markPushedToCentral, + * keyed off the s3Path this returns in each ack) - the central server's own database is + * left untouched. + */ +@Service +public class DiagnosticDocumentIngestService { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + private static final Gson GSON = new Gson(); + private static final String ENCRYPTED_FILE_SUFFIX = ".enc"; + + @Value("${diagnostic.documents.s3.bucket}") + private String bucket; + + @Autowired + private S3Client s3Client; + + public String ingestDocuments(String requestOBJ) throws Exception { + Type listType = new TypeToken>>() { + }.getType(); + List> items = GSON.fromJson(requestOBJ, listType); + if (items == null || items.isEmpty()) { + return GSON.toJson(new ArrayList<>()); + } + + List> acks = new ArrayList<>(); + for (Map item : items) { + acks.add(ingestOne(item)); + } + return GSON.toJson(acks); + } + + private Map ingestOne(Map item) { + Map ack = new HashMap<>(); + ack.put("documentId", item.get("documentId")); + ack.put("diagnosticOrderId", item.get("diagnosticOrderId")); + ack.put("externalOrderId", item.get("externalOrderId")); + ack.put("documentType", item.get("documentType")); + + try { + ack.put("documentId", asLong(item.get("documentId"))); + Long diagnosticOrderId = asLong(item.get("diagnosticOrderId")); + ack.put("diagnosticOrderId", diagnosticOrderId); + String documentType = (String) item.get("documentType"); + + byte[] plaintext = Base64.getDecoder().decode((String) item.get("fileContentBase64")); + + String sha256Hash = (String) item.get("sha256Hash"); + if (sha256Hash != null && !sha256Hash.equalsIgnoreCase(sha256Hex(plaintext))) { + ack.put("status", "FAILED"); + ack.put("error", "sha256 mismatch on receipt"); + return ack; + } + + Long beneficiaryId = asLong(item.get("beneficiaryId")); + Long vanId = asLong(item.get("vanID")); + String orderType = (String) item.get("orderType"); + String storedFileName = (String) item.get("storedFileName"); + if (vanId == null || beneficiaryId == null || orderType == null || documentType == null + || storedFileName == null) { + ack.put("status", "FAILED"); + ack.put("error", "Missing vanID, beneficiaryId, orderType, documentType or storedFileName"); + return ack; + } + String s3Key = vanId + "/" + beneficiaryId + "/" + orderType + "/" + documentType + "/" + + objectFileName(storedFileName, (String) item.get("fileExtension")); + String contentType = (String) item.get("contentType"); + + s3Client.putObject( + PutObjectRequest.builder().bucket(bucket).key(s3Key) + .contentType(contentType != null ? contentType : "application/octet-stream") + .serverSideEncryption(ServerSideEncryption.AES256).build(), + RequestBody.fromBytes(plaintext)); + + ack.put("status", "SUCCESS"); + ack.put("s3Path", s3Key); + } catch (Exception e) { + logger.error("Error ingesting diagnostic document: documentId=" + item.get("documentId") + + ", diagnosticOrderId=" + item.get("diagnosticOrderId") + + ", documentType=" + item.get("documentType"), e); + ack.put("status", "FAILED"); + ack.put("error", e.getMessage()); + } + return ack; + } + + /*** + * @purpose The van stores files encrypted as ".enc", but S3 holds the decrypted + * content - so the object is named with the real extension the van derived from + * its content type (e.g. CAD.enc -> CAD.pdf), not the misleading ".enc". + */ + private static String objectFileName(String storedFileName, String fileExtension) { + String baseName = storedFileName.endsWith(ENCRYPTED_FILE_SUFFIX) + ? storedFileName.substring(0, storedFileName.length() - ENCRYPTED_FILE_SUFFIX.length()) + : storedFileName; + return fileExtension != null && !fileExtension.isEmpty() ? baseName + "." + fileExtension : baseName; + } + + private static Long asLong(Object value) { + return value == null ? null : ((Number) value).longValue(); + } + + private static String sha256Hex(byte[] data) throws Exception { + byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); + StringBuilder hex = new StringBuilder(digest.length * 2); + for (byte b : digest) { + hex.append(String.format(Locale.ROOT, "%02x", b)); + } + return hex.toString(); + } +} diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentPushServiceImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentPushServiceImpl.java new file mode 100644 index 00000000..cc817200 --- /dev/null +++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentPushServiceImpl.java @@ -0,0 +1,274 @@ +package com.iemr.mmu.service.dataSyncLayerCentral; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +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.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; +import com.iemr.mmu.utils.CryptoUtil; +import com.iemr.mmu.utils.RestTemplateUtil; + +import java.lang.reflect.Type; + +/*** + * @purpose Reads MMU-API's own locally-pending diagnostic documents (docsProcessed='N', + * shared db_iemr.tb_diagnostic_document table), decrypts the file off the shared + * filesystem, and pushes each batch to the further central server's + * /dataSync/diagnostic-documents endpoint - the same relay shape as + * UploadDataToServerImpl's push to dataSyncUploadUrl, just for this table instead + * of the generic sync-group config. MMU-API's own /dataSync/diagnostic-documents + * ingest endpoint (DiagnosticDocumentCentralIngestService) is untouched by this - + * this service is a separate outbound relay, not a caller of it. + */ +@Service +public class DiagnosticDocumentPushServiceImpl { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + private static final Map CONTENT_TYPE_EXTENSIONS = new HashMap<>(); + static { + CONTENT_TYPE_EXTENSIONS.put("application/pdf", "pdf"); + CONTENT_TYPE_EXTENSIONS.put("image/jpeg", "jpg"); + CONTENT_TYPE_EXTENSIONS.put("image/png", "png"); + } + + @Value("${diagnostic.documents.storage-root}") + private String storageRoot; + + @Value("${diagnosticDocumentUploadUrl}") + private String diagnosticDocumentUploadUrl; + + @Value("${diagnosticDocument.push.batchSize:3}") + private int batchSize; + + @Autowired + private DiagnosticDocumentRepository diagnosticDocumentRepository; + + @Autowired + private CryptoUtil cryptoUtil; + + public String pushPendingDocuments(String Authorization) throws Exception { + List> pendingRows = diagnosticDocumentRepository.findPendingDocuments(); + int totalSucceeded = 0; + List failureReasons = new ArrayList<>(); + + for (int offset = 0; offset < pendingRows.size(); offset += batchSize) { + List> rows = pendingRows.subList(offset, Math.min(offset + batchSize, pendingRows.size())); + + List> payloadItems = new ArrayList<>(); + Map> rowsById = new HashMap<>(); + for (Map row : rows) { + Long rowId = asLong(row.get("id")); + String base64Plaintext; + try { + String storedPath = (String) row.get("stored_path"); + Path filePath = Paths.get(storageRoot, storedPath); + String encryptedPayload = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8); + base64Plaintext = cryptoUtil.decrypt(encryptedPayload); + } catch (Exception e) { + logger.warn("Skipping diagnostic document push, could not read file off disk: id={}, error={}", + rowId, e.getMessage()); + markFailed(rowId, "Could not read file off shared filesystem: " + e.getMessage(), failureReasons); + continue; + } + if (base64Plaintext == null) { + logger.warn("Skipping diagnostic document push, decrypt failed: id={}", rowId); + markFailed(rowId, "Decrypt failed", failureReasons); + continue; + } + + Long diagnosticOrderId = asLong(row.get("diagnostic_order_id")); + String externalOrderId = (String) row.get("external_order_id"); + String documentType = (String) row.get("document_type"); + String contentType = (String) row.get("content_type"); + + Map item = new HashMap<>(); + item.put("documentId", rowId); + item.put("diagnosticOrderId", diagnosticOrderId); + item.put("externalOrderId", externalOrderId); + item.put("beneficiaryId", row.get("beneficiary_id")); + item.put("orderType", row.get("order_type")); + item.put("documentType", documentType); + item.put("storedFileName", row.get("stored_file_name")); + item.put("sha256Hash", row.get("sha256_hash")); + item.put("contentType", contentType); + item.put("fileExtension", extensionFor(contentType)); + item.put("originalFileName", row.get("original_file_name")); + item.put("vanID", row.get("vanID")); + item.put("parkingPlaceID", row.get("parkingPlaceID")); + item.put("vanSerialNo", row.get("vanSerialNo")); + item.put("fileContentBase64", base64Plaintext); + payloadItems.add(item); + rowsById.put(rowId, row); + } + + if (payloadItems.isEmpty()) { + continue; + } + + String requestOBJ = new Gson().toJson(payloadItems); + List> acks; + try { + HttpEntity request = RestTemplateUtil.createRequestEntity(requestOBJ, Authorization, "datasync"); + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.exchange(diagnosticDocumentUploadUrl, HttpMethod.POST, + request, String.class); + + if (response == null || !response.hasBody()) { + logger.warn("No response body from central server for diagnostic document push, marking batch failed"); + markBatchFailed(rowsById, "No response body from central server", failureReasons); + continue; + } + + // Central server wraps every response in the shared OutputResponse envelope + // ({"data": [...], "statusCode":200, ...}) - the ack array lives under "data". + JsonElement parsedBody = JsonParser.parseString(response.getBody()); + if (!parsedBody.isJsonObject()) { + logger.warn("Unexpected response shape from central server for diagnostic document push, marking batch failed"); + markBatchFailed(rowsById, "Unexpected response shape from central server", failureReasons); + continue; + } + JsonObject envelope = parsedBody.getAsJsonObject(); + if (!envelope.has("statusCode") || envelope.get("statusCode").getAsInt() != 200 + || !envelope.has("data")) { + logger.warn("Central server reported failure for diagnostic document push batch, marking batch failed: {}", + response.getBody()); + markBatchFailed(rowsById, "Central server reported failure: " + response.getBody(), failureReasons); + continue; + } + + Type ackListType = new TypeToken>>() { + }.getType(); + acks = new Gson().fromJson(envelope.get("data"), ackListType); + if (acks == null) { + markBatchFailed(rowsById, "Central server returned no acknowledgements", failureReasons); + continue; + } + } catch (Exception e) { + logger.error("Error calling central server for diagnostic document push, marking batch failed", e); + markBatchFailed(rowsById, "Error calling central server: " + e.getMessage(), failureReasons); + continue; + } + + int batchSuccessCount = 0; + Map> unmatchedRowsById = new HashMap<>(rowsById); + for (Map ack : acks) { + Long rowId = ack.get("documentId") instanceof Number ? asLong(ack.get("documentId")) : null; + if (rowId == null || unmatchedRowsById.remove(rowId) == null) { + continue; + } + if ("SUCCESS".equalsIgnoreCase((String) ack.get("status"))) { + diagnosticDocumentRepository.markPushedToCentral(rowId, (String) ack.get("s3Path")); + batchSuccessCount++; + } else { + String reason = (String) ack.get("error"); + logger.warn("Central server rejected diagnostic document push: id={}, externalOrderId={}, documentType={}, error={}", + rowId, ack.get("externalOrderId"), ack.get("documentType"), reason); + markFailed(rowId, reason != null ? reason : "Central server rejected the document", failureReasons); + } + } + if (!unmatchedRowsById.isEmpty()) { + // Central sent back fewer acks than documents we sent - whatever wasn't + // accounted for must not be silently left at its previous status forever. + logger.warn( + "Diagnostic document push: {} row(s) in this batch got no matching ack back, marking failed", + unmatchedRowsById.size()); + markBatchFailed(unmatchedRowsById, "No acknowledgement received from central server", failureReasons); + } + + totalSucceeded += batchSuccessCount; + logger.info("Diagnostic document push batch complete: attempted={}, succeeded={}", payloadItems.size(), + batchSuccessCount); + } + + if (pendingRows.isEmpty()) { + return "No data to sync"; + } + + // Same table-level summary shape as /van-to-server (UploadDataToServerImpl) so the + // data sync screen can read both the same way. + int totalRecords = pendingRows.size(); + int failedRecords = failureReasons.size(); + String status; + if (failedRecords == 0) { + status = "success"; + } else if (totalSucceeded == 0) { + status = "failed"; + } else { + status = "partial"; + } + + Map finalResponse = new LinkedHashMap<>(); + finalResponse.put("response", + "success".equals(status) ? "Data sync completed successfully" : "Data sync completed with failures"); + finalResponse.put("schemaName", "db_iemr"); + finalResponse.put("tableName", "tb_diagnostic_document"); + finalResponse.put("status", status); + finalResponse.put("totalRecords", totalRecords); + finalResponse.put("successfulRecords", totalSucceeded); + finalResponse.put("failedRecords", failedRecords); + if (!failureReasons.isEmpty()) { + finalResponse.put("failureReasons", failureReasons); + } + + logger.info("Diagnostic document push complete: {} (Success: {}, Failed: {}, Total: {})", status, + totalSucceeded, failedRecords, totalRecords); + return new Gson().toJson(finalResponse); + } + + /*** + * @purpose Marks one row failed locally and records its reason for the run summary. + */ + private void markFailed(Long rowId, String reason, List failureReasons) { + diagnosticDocumentRepository.markPushFailed(rowId, reason); + failureReasons.add("documentId " + rowId + ": " + reason); + } + + /*** + * @purpose Called when the whole batch call to the central server fails (no/garbled + * response, non-200, or a thrown exception) - marks every row that was in this + * batch as failed, matching how /van-to-server marks a whole batch failed on a + * connection error, rather than leaving them stuck at docsProcessed='N' forever. + */ + private void markBatchFailed(Map> rowsById, String reason, List failureReasons) { + for (Long rowId : rowsById.keySet()) { + markFailed(rowId, reason, failureReasons); + } + } + + private static Long asLong(Object value) { + return value == null ? null : ((Number) value).longValue(); + } + + private static String extensionFor(String contentType) { + if (contentType == null) { + return "bin"; + } + String extension = CONTENT_TYPE_EXTENSIONS.get(contentType.toLowerCase()); + if (extension != null) { + return extension; + } + int slashIndex = contentType.indexOf('/'); + return slashIndex >= 0 && slashIndex < contentType.length() - 1 ? contentType.substring(slashIndex + 1) : "bin"; + } +} diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentRepository.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentRepository.java new file mode 100644 index 00000000..53c30c5f --- /dev/null +++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/DiagnosticDocumentRepository.java @@ -0,0 +1,95 @@ +package com.iemr.mmu.service.dataSyncLayerCentral; + +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +/*** + * @purpose Local (van) side persistence for the diagnostic-document push pipeline - the central + * server is a pure S3 storage relay (DiagnosticDocumentCentralIngestService) and writes + * nothing to its own database, so this repository only ever operates on the pushing + * van's own local db_iemr.tb_diagnostic_document rows. + */ +@Service +public class DiagnosticDocumentRepository { + + @Autowired + private DataSource dataSource; + + private JdbcTemplate jdbcTemplate; + + private JdbcTemplate getJdbcTemplate() { + if (this.jdbcTemplate == null) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + return this.jdbcTemplate; + } + + /*** + * @purpose Latest successfully-pushed document for a beneficiary+documentType, used to hand + * back a fresh presigned URL on demand instead of persisting a permanent (and, for a + * private bucket, non-functional) URL. s3_path holds the S3 key for a + * docsProcessed='P' row (markPushedToCentral() below writes it there). + */ + public Map findLatestDocument(Long beneficiaryId, String documentType) { + String query = "SELECT id, external_order_id, order_type, document_type, s3_path, content_type, " + + "original_file_name, last_mod_date FROM db_iemr.tb_diagnostic_document " + + "WHERE beneficiary_id = ? AND document_type = ? AND docsProcessed = 'P' " + + "ORDER BY last_mod_date DESC, id DESC LIMIT 1"; + List> rows = getJdbcTemplate().queryForList(query, beneficiaryId, documentType); + return rows.isEmpty() ? null : rows.get(0); + } + + /*** + * @purpose Snapshot of every locally-pending row's metadata (docsProcessed='N' or 'F') at + * the start of a push run, taken in one query so a row this same run marks 'F' + * partway through (e.g. an auth failure) is never re-picked-up later in the same + * run - it only becomes eligible again on the next trigger, alongside whatever's + * newly 'N' by then. Only metadata is loaded here (id, hashes, filenames, etc.) - + * each row's actual file content is still read off disk and decrypted one batch at + * a time by the caller, so the whole backlog's decrypted content is never held in + * memory at once. + */ + public List> findPendingDocuments() { + String query = "SELECT id, diagnostic_order_id, external_order_id, beneficiary_id, order_type, document_type, " + + "stored_file_name, stored_path, sha256_hash, content_type, original_file_name, " + + "vanID, parkingPlaceID, vanSerialNo FROM db_iemr.tb_diagnostic_document " + + "WHERE docsProcessed = 'N' OR docsProcessed = 'F' ORDER BY id ASC"; + return getJdbcTemplate().queryForList(query); + } + + /*** + * @purpose Marks a document as successfully pushed on the LOCAL (van) row - the central + * server itself writes nothing to its own database. s3Path is the S3 key the + * central server's ack reported back, persisted here so the van's own local DB + * knows where the document ended up, not just that it did. + */ + public void markPushedToCentral(Long id, String s3Path) { + String update = "UPDATE db_iemr.tb_diagnostic_document SET processed = 'N', docsProcessed = 'P', " + + "s3_path = ?, docSyncedDate = NOW(), docSyncFailureReason = NULL, last_mod_date = NOW() WHERE id = ?"; + getJdbcTemplate().update(update, s3Path, id); + } + + /*** + * @purpose Marks a document push to the further central server as failed - docsProcessed='F', + * matching the P/F status convention used by the generic sync pipeline's + * DataSyncRepository.updateProcessedFlagInVan for /van-to-server. Persists why in + * docSyncFailureReason (mirroring the generic pipeline's own SyncFailureReason, kept + * separate since this pipeline's docsProcessed/docSyncedDate are their own dedicated + * columns) so a failure isn't only visible in the application log. Also clears + * s3_path - otherwise a row that succeeded once, then got re-attempted and failed, + * would keep showing a stale S3 key while docsProcessed says 'F'. docSyncedDate is + * left untouched - it records the last time this row was actually confirmed synced, + * if ever. + */ + public void markPushFailed(Long id, String reason) { + String update = "UPDATE db_iemr.tb_diagnostic_document SET processed = 'N', docsProcessed = 'F', " + + "s3_path = NULL, docSyncFailureReason = ?, last_mod_date = NOW() WHERE id = ?"; + getJdbcTemplate().update(update, reason, id); + } +} diff --git a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImpl.java b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImpl.java index c0e508cd..4db30fff 100644 --- a/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImpl.java +++ b/src/main/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImpl.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -53,6 +54,21 @@ public class GetDataFromVanAndSyncToDBImpl implements GetDataFromVanAndSyncToDB @Value("${downSync.markDeliveredOnUpSync:true}") private boolean markDownSyncedOnUpSync; + // fill VanSerialNo / SyncFacilityID from the van row when not sent by name (see fillVanKeys) + @Value("${dataSync.fillVanKeys:true}") + private boolean fillVanKeysEnabled = true; + + // replace van IDs in link columns by central IDs (see translateVanLinks); off by default, on for StopTB central + @Value("${dataSync.translateVanLinks:false}") + private boolean translateVanLinksEnabled; + + // link column -> {table it points to, that table's ID column} + private static final Map VAN_LINKS = new LinkedHashMap<>(); + static { + VAN_LINKS.put("BenVisitID", new String[] { "t_benvisitdetail", "BenVisitID" }); + VAN_LINKS.put("PrescriptionID", new String[] { "t_prescription", "PrescriptionID" }); + } + private static final Map> TABLE_GROUPS = new HashMap<>(); static { TABLE_GROUPS.put(1, @@ -372,7 +388,21 @@ private boolean performGenericTableSync(SyncUploadDataDigester syncUploadDataDig // CRITICAL FIX: Keep null as null, don't convert to string cleanRecord.put(cleanKey.trim(), value); } - + if (fillVanKeysEnabled) + fillVanKeys(cleanRecord, serverColumnsList, vanAutoIncColumnName); + if (translateVanLinksEnabled) { + Object linkVanID = cleanRecord.get("VanID") != null ? cleanRecord.get("VanID") : cleanRecord.get("vanID"); + String linkError = translateVanLinks(schemaName, syncTableName, cleanRecord, serverColumnsList, + linkVanID == null ? null : String.valueOf(linkVanID)); + if (linkError != null) { + syncResults.add(new SyncResult(schemaName, syncTableName, + String.valueOf(cleanRecord.get(vanAutoIncColumnName)), syncUploadDataDigester.getSyncedBy(), + false, linkError)); + overallSuccess = false; + continue; + } + } + String vanSerialNo = String.valueOf(cleanRecord.get(vanAutoIncColumnName)); // Column may be "vanID" (Stop TB tables) or "VanID" (standard tables) — check both @@ -606,6 +636,57 @@ private boolean performGenericTableSync(SyncUploadDataDigester syncUploadDataDig return overallSuccess; } + /** + * m_synctabledetail maps van columns to server columns by position: the van's own ID (VanAutoIncColumnName, + * e.g. BenVisitID, ItemStockEntryID) sits where the server list has VanSerialNo. Records are filled by name, so + * without this VanSerialNo stays NULL: the row cannot be matched on the next upload (duplicates) and joins on + * VanSerialNo break. Same for SyncFacilityID, which some van lists do not send: it is the row's store. + */ + static void fillVanKeys(Map record, List serverColumns, String vanAutoIncColumnName) { + if (hasColumn(serverColumns, "VanSerialNo") && record.get("VanSerialNo") == null + && vanAutoIncColumnName != null && record.get(vanAutoIncColumnName) != null) { + record.put("VanSerialNo", record.get(vanAutoIncColumnName)); + } + if (hasColumn(serverColumns, "SyncFacilityID") && record.get("SyncFacilityID") == null + && record.get("FacilityID") != null) { + record.put("SyncFacilityID", record.get("FacilityID")); + } + } + + /** + * A van sends its own IDs in link columns (BenVisitID, PrescriptionID). On central those numbers belong to other + * rows (other vans, central data): the link pointed to the wrong visit/prescription, or the foreign key rejected + * the row. The link is replaced by the central ID of the row the van uploaded with that VanSerialNo. If that row + * is not on central yet, the record is not saved (returned reason) and is sent again on the next upload. + * + * @return null when done, or the reason the record cannot be saved yet + */ + String translateVanLinks(String schemaName, String tableName, Map record, + List serverColumns, String vanID) { + for (Map.Entry link : VAN_LINKS.entrySet()) { + String column = link.getKey(); + String parentTable = link.getValue()[0]; + Object vanValue = record.get(column); + if (vanValue == null || vanID == null || tableName.equalsIgnoreCase(parentTable) + || !hasColumn(serverColumns, column)) + continue; + Long centralID = dataSyncRepositoryCentral.findCentralIdOfVanRow(schemaName, parentTable, + link.getValue()[1], vanValue, vanID); + if (centralID == null) + return "Linked " + parentTable + " (van ID " + vanValue + ") not on central yet; sent again on next upload"; + record.put(column, centralID); + } + return null; + } + + private static boolean hasColumn(List columns, String name) { + for (String column : columns) { + if (column.trim().equals(name)) + return true; + } + return false; + } + private void flagUpSyncedRecordsForDownSync(String schemaName, String tableName, List syncResults, Map vanIDByResultIndex) { if (!markDownSyncedOnUpSync || vanIDByResultIndex.isEmpty()) diff --git a/src/main/java/com/iemr/mmu/utils/CryptoUtil.java b/src/main/java/com/iemr/mmu/utils/CryptoUtil.java new file mode 100644 index 00000000..c964fdf2 --- /dev/null +++ b/src/main/java/com/iemr/mmu/utils/CryptoUtil.java @@ -0,0 +1,42 @@ +package com.iemr.mmu.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Decrypts diagnostic-document files written by FLW-API's own CryptoUtil + * (same AES/ECB/PKCS5Padding scheme and key) so they can be read directly + * off the shared filesystem before pushing to the further central server. + */ +@Service +public class CryptoUtil { + + private static final Logger logger = LoggerFactory.getLogger(CryptoUtil.class); + private static final String ALGORITHM = "AES"; + private static final String SECRET_KEY = "dev-envro-secret"; + + public String decrypt(String encryptedValue) { + try { + SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedValue)); + return removePadding(new String(decryptedBytes, StandardCharsets.UTF_8)); + } catch (Exception e) { + logger.error("Exception while decrypting diagnostic document", e); + return null; + } + } + + private String removePadding(String value) { + int paddingLength = value.charAt(value.length() - 1); + return value.substring(0, value.length() - paddingLength); + } +} diff --git a/src/test/java/com/iemr/mmu/service/benid/BenIdAutoTopUpServiceTest.java b/src/test/java/com/iemr/mmu/service/benid/BenIdAutoTopUpServiceTest.java new file mode 100644 index 00000000..c10e6230 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/benid/BenIdAutoTopUpServiceTest.java @@ -0,0 +1,272 @@ +/* +* 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.mmu.service.benid; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.client.ExpectedCount; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +class BenIdAutoTopUpServiceTest { + + private static final String GENERATE = "http://central/common-api/beneficiary/generateBeneficiaryIDs"; + private static final String IMPORT = "http://local/id/saveGeneratedBenIDToLocalServer"; + private static final String COUNT = "http://local/id/checkAvailablBenIDLocalServer"; + + @TempDir + Path dataDir; + + private BenIdAutoTopUpService service; + private CentralAuthService centralAuth; + private MockRestServiceServer server; + + @BeforeEach + void setUp() throws Exception { + centralAuth = mock(CentralAuthService.class); + when(centralAuth.isEnabled()).thenReturn(true); + when(centralAuth.getVanID()).thenReturn(403); + when(centralAuth.getState()).thenReturn(CentralAuthService.State.CONNECTED); + when(centralAuth.getCentralSessionKey()).thenReturn("SESSION-KEY"); + + service = new BenIdAutoTopUpService(); + ReflectionTestUtils.setField(service, "centralAuthService", centralAuth); + ReflectionTestUtils.setField(service, "benGenUrlCentral", GENERATE); + ReflectionTestUtils.setField(service, "benImportUrlLocal", IMPORT); + ReflectionTestUtils.setField(service, "benCheckUrlLocal", COUNT); + ReflectionTestUtils.setField(service, "lowerLimit", 2000L); + ReflectionTestUtils.setField(service, "batchSize", 1000L); + ReflectionTestUtils.setField(service, "bootstrapSize", 10000L); + ReflectionTestUtils.setField(service, "maxPerRequest", 1000L); + ReflectionTestUtils.setField(service, "dataDir", dataDir.toString()); + ReflectionTestUtils.setField(service, "retryDelaysMs", new long[] { 0L }); + ReflectionTestUtils.setField(service, "connectTimeoutMs", 1000); + ReflectionTestUtils.setField(service, "readTimeoutMs", 1000); + service.init(); + server = MockRestServiceServer.bindTo((RestTemplate) ReflectionTestUtils.getField(service, "restTemplate")) + .build(); + } + + @AfterEach + void releaseLock() { + while (BenIdAutoTopUpService.GENERATION_LOCK.isHeldByCurrentThread()) { + BenIdAutoTopUpService.GENERATION_LOCK.unlock(); + } + } + + private void expectCount(long n) { + server.expect(requestTo(COUNT)) + .andRespond(withSuccess("{\"statusCode\":200,\"data\":{\"response\":\"" + n + "\"}}", + MediaType.APPLICATION_JSON)); + } + + private static String ids(int n) { + StringBuilder sb = new StringBuilder("["); + for (int i = 1; i <= n; i++) { + sb.append(i > 1 ? "," : "").append("{\"benRegId\":").append(5000 + i) + .append(",\"beneficiaryId\":").append(900000000000L + i).append("}"); + } + return sb.append("]").toString(); + } + + private void expectGenerate(long requested, int returned) { + server.expect(requestTo(GENERATE)).andExpect(header("Authorization", "SESSION-KEY")) + .andExpect(content().string(Matchers.containsString("\"benIDRequired\":" + requested))) + .andExpect(content().string(Matchers.containsString("\"vanID\":403"))) + .andRespond(withSuccess("{\"statusCode\":200,\"data\":" + ids(returned) + "}", + MediaType.APPLICATION_JSON)); + } + + private void expectImportOk() { + server.expect(requestTo(IMPORT)).andExpect(content().string(Matchers.containsString("\"vanID\":403"))) + .andRespond(withSuccess("{\"statusCode\":200,\"data\":{\"response\":\"ok\"}}", + MediaType.APPLICATION_JSON)); + } + + private void run() { + ReflectionTestUtils.invokeMethod(service, "runOnce", true); + } + + private Path pending() { + return dataDir.resolve("benid-pending-import.json"); + } + + @Test + void doesNothingWhenDisabled() throws Exception { + when(centralAuth.isEnabled()).thenReturn(false); + server.expect(ExpectedCount.never(), requestTo(COUNT)); + run(); + server.verify(); + verify(centralAuth, never()).getCentralSessionKey(); + } + + @Test + void emptyPoolGetsBootstrapSize() { + ReflectionTestUtils.setField(service, "bootstrapSize", 800L); + expectCount(0); + expectGenerate(800, 3); + expectImportOk(); + expectCount(3); + run(); + server.verify(); + assertFalse(Files.exists(pending()), "pending file removed after a successful import"); + } + + @Test + void lowPoolGetsBatchSize() { + expectCount(1999); + expectGenerate(1000, 2); + expectImportOk(); + expectCount(2001); + run(); + server.verify(); + } + + @Test + void healthyPoolDoesNotCallCentral() throws Exception { + expectCount(2000); + server.expect(ExpectedCount.never(), requestTo(GENERATE)); + run(); + server.verify(); + verify(centralAuth, never()).getCentralSessionKey(); + } + + @Test + void noCentralTokenWaitsForLogin() throws Exception { + when(centralAuth.getCentralSessionKey()).thenReturn(null); + expectCount(0); + server.expect(ExpectedCount.never(), requestTo(GENERATE)); + run(); + server.verify(); + assertTrue(((String) ReflectionTestUtils.getField(service, "lastError")).contains("waiting for a user login")); + } + + @Test + void centralErrorLeavesNothingBehind() { + expectCount(0); + server.expect(requestTo(GENERATE)).andRespond(withStatus(HttpStatus.GATEWAY_TIMEOUT)); + run(); + server.verify(); + assertFalse(Files.exists(pending())); + assertTrue(ReflectionTestUtils.getField(service, "lastError") != null); + } + + @Test + void failedLocalImportKeepsIdsAndImportsThemNextRun() throws Exception { + ReflectionTestUtils.setField(service, "bootstrapSize", 800L); + expectCount(0); + expectGenerate(800, 3); + server.expect(requestTo(IMPORT)).andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR)); + // next run: pending IDs are imported first, then the pool is healthy + expectImportOk(); + expectCount(2500); + run(); + assertTrue(Files.exists(pending()), "IDs received from central must be kept"); + assertEquals(3, Files.readString(pending()).split("benRegId").length - 1); + + run(); + server.verify(); + assertFalse(Files.exists(pending())); + } + + @Test + void skipsWhileManualGenerationRuns() { + BenIdAutoTopUpService.GENERATION_LOCK.lock(); + try { + Thread other = new Thread(this::run); + server.expect(ExpectedCount.never(), requestTo(COUNT)); + other.start(); + other.join(5000); + server.verify(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + BenIdAutoTopUpService.GENERATION_LOCK.unlock(); + } + } + + @Test + void bigDownloadIsSplitIntoRequestsOfAtMost1000() { + ReflectionTestUtils.setField(service, "bootstrapSize", 2500L); + expectCount(0); + expectGenerate(1000, 2); + expectImportOk(); + expectGenerate(1000, 2); + expectImportOk(); + expectGenerate(500, 2); + expectImportOk(); + expectCount(2500); + run(); + server.verify(); + } + + @Test + void pendingFileThatKeepsFailingIsSetAsideAndTopUpsContinue() throws Exception { + Files.writeString(pending(), "[{\"benRegId\":12"); // half-written by a power cut + for (int i = 0; i < 3; i++) { + server.expect(requestTo(IMPORT)).andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR)); + } + expectCount(2500); + run(); + run(); + assertTrue(Files.exists(pending()), "kept while retries are left"); + run(); + server.verify(); + assertFalse(Files.exists(pending()), "set aside after 3 failures"); + try (java.util.stream.Stream files = Files.list(dataDir)) { + assertTrue(files.anyMatch(f -> f.getFileName().toString().startsWith("benid-pending-import.failed-"))); + } + } + + @Test + void retryWaitIsSpreadPlusMinus20Percent() { + ReflectionTestUtils.setField(service, "retryDelaysMs", new long[] { 10000L }); + for (int i = 0; i < 20; i++) { + long before = System.currentTimeMillis(); + ReflectionTestUtils.invokeMethod(service, "onFailure", new RuntimeException("down")); + long wait = (long) ReflectionTestUtils.getField(service, "nextAttemptAt") - before; + assertTrue(wait >= 7990 && wait <= 12010, "wait " + wait + " ms is outside 8-12 s"); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/benid/CentralAuthServiceTest.java b/src/test/java/com/iemr/mmu/service/benid/CentralAuthServiceTest.java new file mode 100644 index 00000000..d6ba45a0 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/benid/CentralAuthServiceTest.java @@ -0,0 +1,276 @@ +/* +* 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.mmu.service.benid; + +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.springframework.test.web.client.match.MockRestRequestMatchers.content; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.client.ExpectedCount; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +class CentralAuthServiceTest { + + private static final String LOGIN = "http://central/common-api/user/userAuthenticate"; + private static final String REFRESH = "http://central/common-api/user/refreshToken"; + private static final String SESSION = "http://central/common-api/user/getLoginResponse"; + + @TempDir + Path tokenDir; + + private CentralAuthService service; + private MockRestServiceServer central; + + @BeforeEach + void setUp() { + service = newService(true, 403); + } + + private CentralAuthService newService(boolean enforce, int vanID) { + CentralAuthService s = new CentralAuthService(); + ReflectionTestUtils.setField(s, "enforceVanID", enforce); + ReflectionTestUtils.setField(s, "vanID", vanID); + ReflectionTestUtils.setField(s, "centralLoginUrl", LOGIN); + ReflectionTestUtils.setField(s, "centralRefreshTokenUrl", REFRESH); + ReflectionTestUtils.setField(s, "centralSessionUrl", SESSION); + ReflectionTestUtils.setField(s, "tokenDir", tokenDir.toString()); + ReflectionTestUtils.setField(s, "tokenSecret", "test-secret"); + ReflectionTestUtils.setField(s, "connectTimeoutMs", 1000); + ReflectionTestUtils.setField(s, "readTimeoutMs", 1000); + s.init(); + RestTemplate rt = (RestTemplate) ReflectionTestUtils.getField(s, "restTemplate"); + central = MockRestServiceServer.bindTo(rt).build(); + return s; + } + + private void expectLoginOk(String refreshToken) { + central.expect(requestTo(LOGIN)).andExpect(method(HttpMethod.POST)) + .andExpect(header("User-Agent", "okhttp/4.12.0")) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"doLogout\":false"))) + .andRespond(withSuccess("{\"statusCode\":200,\"data\":{\"refreshToken\":\"" + refreshToken + + "\",\"key\":\"k\"}}", MediaType.APPLICATION_JSON)); + } + + private void expectLoginRejected() { + central.expect(requestTo(LOGIN)).andRespond(withSuccess( + "{\"statusCode\":5002,\"errorMessage\":\"Invalid username or password\"}", MediaType.APPLICATION_JSON)); + } + + @Test + void disabledWithoutVanId() { + CentralAuthService off = newService(true, 0); + assertFalse(off.isEnabled()); + assertEquals(CentralAuthService.State.DISABLED, off.getState()); + assertFalse(off.capture("stoptb1", "pw", true)); + central.verify(); // nothing sent to central + } + + @Test + void disabledWhenEnforceFlagOff() { + CentralAuthService off = newService(false, 403); + assertFalse(off.isEnabled()); + assertFalse(off.capture("stoptb1", "pw", true)); + central.verify(); + } + + @Test + void captureStoresOnlyEncryptedTokenWithOwnerOnlyPermissions() throws Exception { + expectLoginOk("RT-secret-123"); + + assertTrue(service.capture("stoptb1", "encrypted-pw", false)); + + Path file = tokenDir.resolve("central-refresh-token"); + String stored = Files.readString(file, StandardCharsets.UTF_8); + assertFalse(stored.contains("RT-secret-123"), "token must not be stored in plain text"); + assertFalse(stored.contains("encrypted-pw"), "password must never be stored"); + assertEquals("rw-------", PosixFilePermissions.toString(Files.getPosixFilePermissions(file))); + assertEquals(CentralAuthService.State.CONNECTED, service.getState()); + central.verify(); + } + + @Test + void tokenIsReadBackAfterRestart() { + expectLoginOk("RT-1"); + service.capture("stoptb1", "pw", false); + + CentralAuthService restarted = newService(true, 403); + assertTrue(restarted.hasToken()); + assertEquals(CentralAuthService.State.CONNECTED, restarted.getState()); + } + + @Test + void corruptTokenFileMeansNewLoginNeeded() throws Exception { + Files.writeString(tokenDir.resolve("central-refresh-token"), "not-a-valid-token"); + CentralAuthService s = newService(true, 403); + assertFalse(s.hasToken()); + assertEquals(CentralAuthService.State.NOT_CONNECTED, s.getState()); + } + + @Test + void secondLoginIsSkippedWhileTokenWorks() { + expectLoginOk("RT-1"); + service.capture("stoptb1", "pw", false); + + assertFalse(service.capture("stoptb1", "pw", false)); + central.verify(); // only one central login + } + + @Test + void rejectedPasswordIsNeverSentAgainWithoutReconnect() { + expectLoginRejected(); + + assertFalse(service.capture("stoptb1", "wrong", false)); + assertEquals(CentralAuthService.State.PASSWORD_MISMATCH, service.getState()); + // next laptop login with the same user must not reach central (account lock risk) + assertFalse(service.capture("stoptb1", "wrong", false)); + central.verify(); + } + + @Test + void dataSyncReconnectRetriesAfterRejection() { + expectLoginRejected(); + expectLoginOk("RT-2"); + + service.capture("stoptb1", "wrong", false); + assertTrue(service.capture("stoptb1", "central-pw", true)); + assertEquals(CentralAuthService.State.CONNECTED, service.getState()); + central.verify(); + } + + @Test + void centralDownIsNotTreatedAsWrongPassword() { + central.expect(requestTo(LOGIN)).andRespond(withStatus(HttpStatus.BAD_GATEWAY)); + expectLoginOk("RT-3"); + + assertFalse(service.capture("stoptb1", "pw", false)); + // network problem: the next login tries again + assertTrue(service.capture("stoptb1", "pw", false)); + central.verify(); + } + + @Test + void sessionKeyFromRefreshTokenAndTokenRotation() throws Exception { + expectLoginOk("RT-old"); + central.expect(requestTo(REFRESH)) + .andExpect(content().string(org.hamcrest.Matchers.containsString("RT-old"))) + .andRespond(withSuccess("{\"jwtToken\":\"JWT-1\",\"refreshToken\":\"RT-new\"}", + MediaType.APPLICATION_JSON)); + central.expect(requestTo(SESSION)).andExpect(header("Jwttoken", "JWT-1")) + .andRespond(withSuccess("{\"statusCode\":200,\"data\":{\"key\":\"SESSION-KEY\"}}", + MediaType.APPLICATION_JSON)); + // the next refresh must use the rotated token + central.expect(requestTo(REFRESH)) + .andExpect(content().string(org.hamcrest.Matchers.containsString("RT-new"))) + .andRespond(withSuccess("{\"jwtToken\":\"JWT-2\"}", MediaType.APPLICATION_JSON)); + central.expect(requestTo(SESSION)).andRespond(withSuccess( + "{\"statusCode\":200,\"data\":\"{\\\"key\\\":\\\"KEY-2\\\"}\"}", MediaType.APPLICATION_JSON)); + + service.capture("stoptb1", "pw", false); + String fileBefore = Files.readString(tokenDir.resolve("central-refresh-token")); + + assertEquals("SESSION-KEY", service.getCentralSessionKey()); + assertFalse(fileBefore.equals(Files.readString(tokenDir.resolve("central-refresh-token"))), + "rotated refresh token must be saved"); + assertEquals("KEY-2", service.getCentralSessionKey()); + central.verify(); + } + + @Test + void revokedTokenIsDeletedAndReconnectNeeded() { + expectLoginOk("RT-1"); + central.expect(requestTo(REFRESH)).andRespond(withStatus(HttpStatus.UNAUTHORIZED)); + + service.capture("stoptb1", "pw", false); + + assertNull(assertDoesNotThrowValue()); + assertFalse(service.hasToken()); + assertFalse(Files.exists(tokenDir.resolve("central-refresh-token"))); + assertEquals(CentralAuthService.State.RECONNECT_NEEDED, service.getState()); + central.verify(); + } + + @Test + void noTokenMeansNoCentralCall() throws Exception { + central.expect(ExpectedCount.never(), requestTo(REFRESH)); + assertNull(service.getCentralSessionKey()); + central.verify(); + } + + private String assertDoesNotThrowValue() { + try { + return service.getCentralSessionKey(); + } catch (Exception e) { + throw new AssertionError("revoked token must not throw", e); + } + } + + @Test + void rejectedPasswordIsRememberedAfterRestart() { + expectLoginRejected(); + service.capture("stoptb1", "wrong", false); + central.verify(); + + CentralAuthService restarted = newService(true, 403); + // laptop login after the restart must not reach central with the rejected password + assertFalse(restarted.capture("stoptb1", "wrong", false)); + central.verify(); + } + + @Test + void successfulReconnectClearsTheRejectedMarker() { + expectLoginRejected(); + expectLoginOk("RT-9"); + service.capture("stoptb1", "wrong", false); + assertTrue(service.capture("stoptb1", "central-pw", true)); + assertFalse(Files.exists(tokenDir.resolve("central-login-rejected"))); + central.verify(); + } + + @Test + void rejectedMarkerHoldsNoPassword() throws Exception { + expectLoginRejected(); + service.capture("stoptb1", "secret-pw", false); + String marker = Files.readString(tokenDir.resolve("central-login-rejected")); + assertEquals("stoptb1", marker.trim()); + assertFalse(marker.contains("secret-pw")); + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncServiceTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncServiceTest.java new file mode 100644 index 00000000..07fa285f --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/AutoSyncServiceTest.java @@ -0,0 +1,176 @@ +/* +* 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.mmu.service.dataSyncActivity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.mmu.service.benid.BenIdAutoTopUpService; +import com.iemr.mmu.service.benid.CentralAuthService; + +class AutoSyncServiceTest { + + private AutoSyncService service; + private CentralAuthService centralAuth; + private UploadDataToServerImpl upload; + private DownloadDataFromServerImpl download; + private DataSyncRepository repo; + private BenIdAutoTopUpService benId; + private DownloadDataFromServerTransactionalImpl inventory; + + @BeforeEach + void setUp() throws Exception { + centralAuth = mock(CentralAuthService.class); + upload = mock(UploadDataToServerImpl.class); + download = mock(DownloadDataFromServerImpl.class); + repo = mock(DataSyncRepository.class); + benId = mock(BenIdAutoTopUpService.class); + inventory = mock(DownloadDataFromServerTransactionalImpl.class); + when(centralAuth.isEnabled()).thenReturn(true); + when(centralAuth.getVanID()).thenReturn(403); + when(centralAuth.getConnectedUser()).thenReturn("stoptb1"); + when(centralAuth.getCentralSessionKey()).thenReturn("SESSION-KEY"); + when(repo.getVanProviderServiceMapID(403)).thenReturn(1726); + + service = new AutoSyncService(); + ReflectionTestUtils.setField(service, "centralAuthService", centralAuth); + ReflectionTestUtils.setField(service, "uploadDataToServerImpl", upload); + ReflectionTestUtils.setField(service, "downloadDataFromServerImpl", download); + ReflectionTestUtils.setField(service, "dataSyncRepository", repo); + ReflectionTestUtils.setField(service, "benIdAutoTopUpService", benId); + ReflectionTestUtils.setField(service, "downloadDataFromServerTransactional", inventory); + ReflectionTestUtils.setField(service, "inventoryDownloadEnabled", true); + ReflectionTestUtils.setField(service, "uploadEnabled", true); + ReflectionTestUtils.setField(service, "masterDownloadEnabled", true); + ReflectionTestUtils.setField(service, "masterDownloadInventoryOnly", true); + } + + @Test + void uploadUsesCentralKeyAndConnectedOperator() throws Exception { + when(upload.getDataToSyncToServer(403, "stoptb1", "SESSION-KEY", null)).thenReturn("Data successfully synced"); + + service.uploadOnce(); + + verify(upload).getDataToSyncToServer(403, "stoptb1", "SESSION-KEY", null); + verify(benId).triggerNow(); + assertEquals("Data successfully synced", service.status().get("lastUploadResult")); + } + + @Test + void uploadIsFollowedByInventoryDownload() throws Exception { + when(upload.getDataToSyncToServer(403, "stoptb1", "SESSION-KEY", null)).thenReturn("Data successfully synced"); + when(inventory.downloadTransactionalData(403, "SESSION-KEY", "datasync")).thenReturn(1); + + service.uploadOnce(); + + verify(inventory).downloadTransactionalData(403, "SESSION-KEY", "datasync"); + assertEquals("success", service.status().get("lastInventoryDownloadResult")); + } + + @Test + void inventoryDownloadFailureKeepsUploadResult() throws Exception { + when(upload.getDataToSyncToServer(403, "stoptb1", "SESSION-KEY", null)).thenReturn("Data successfully synced"); + when(inventory.downloadTransactionalData(403, "SESSION-KEY", "datasync")).thenThrow(new RuntimeException("offline")); + + service.uploadOnce(); + + assertEquals("Data successfully synced", service.status().get("lastUploadResult")); + assertEquals("Inventory download: offline", service.status().get("lastError")); + } + + @Test + void noCentralConnectionSkipsUpload() throws Exception { + when(centralAuth.getCentralSessionKey()).thenReturn(null); + + service.uploadOnce(); + + verify(upload, never()).getDataToSyncToServer(anyInt(), anyString(), anyString(), any()); + assertEquals("No central connection - waiting for a user login", service.status().get("lastError")); + } + + @Test + void masterDownloadUsesVanServiceLineAndOnlyInventoryMasters() throws Exception { + when(download.downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, AutoSyncService.INVENTORY_MASTERS)) + .thenReturn("success"); + + service.masterDownloadOnce(); + + verify(download).downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, AutoSyncService.INVENTORY_MASTERS); + assertEquals("success", service.status().get("lastMasterDownloadResult")); + } + + @Test + void masterDownloadCanFetchAllMasters() throws Exception { + ReflectionTestUtils.setField(service, "masterDownloadInventoryOnly", false); + when(download.downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, null)).thenReturn("success"); + + service.masterDownloadOnce(); + + verify(download).downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, null); + } + + @Test + void manualMasterDownloadRunningIsNotCountedAsARun() throws Exception { + when(download.downloadMasterDataFromServer("SESSION-KEY", null, 403, 1726, AutoSyncService.INVENTORY_MASTERS)) + .thenReturn("inProgress"); + + service.masterDownloadOnce(); + + assertEquals(0L, service.status().getLong("lastMasterDownloadAt")); + } + + @Test + void statusSumsRejectedRows() throws Exception { + when(repo.getSyncFailures()).thenReturn(List.of( + Map.of("tableName", "t_patientissue", "reason", "Duplicate", "failedRows", 2L), + Map.of("tableName", "t_prescription", "reason", "Timeout", "failedRows", 3L))); + + JSONObject status = service.status(); + + assertEquals(5L, status.getLong("failedRows")); + assertEquals(2, status.getJSONArray("failures").length()); + } + + @Test + void disabledLaptopDoesNothing() throws Exception { + when(centralAuth.isEnabled()).thenReturn(false); + + service.scheduledUpload(); + service.scheduledMasterDownload(); + + verify(centralAuth, never()).getCentralSessionKey(); + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadMastersFullForLineTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadMastersFullForLineTest.java new file mode 100644 index 00000000..d92182b2 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadMastersFullForLineTest.java @@ -0,0 +1,49 @@ +package com.iemr.mmu.service.dataSyncActivity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.sql.Timestamp; + +import org.junit.jupiter.api.Test; + +import com.iemr.mmu.data.syncActivity_syncLayer.SyncDownloadMaster; + +class DownloadMastersFullForLineTest { + + private static final Timestamp COPIED_DATE = Timestamp.valueOf("2026-10-16 13:17:31"); + + private static SyncDownloadMaster table(String masterType) { + SyncDownloadMaster t = new SyncDownloadMaster(); + t.setMasterType(masterType); + t.setLastDownloadDate(COPIED_DATE); + return t; + } + + @Test + void serviceLineMastersAreDownloadedInFull() { + SyncDownloadMaster parkingPlace = table("P"); + + DownloadDataFromServerImpl.fullDownloadForLineAndVanMasters(parkingPlace); + + assertNull(parkingPlace.getLastDownloadDate()); + } + + @Test + void vanMastersAreDownloadedInFull() { + SyncDownloadMaster vanServicePoint = table("v"); + + DownloadDataFromServerImpl.fullDownloadForLineAndVanMasters(vanServicePoint); + + assertNull(vanServicePoint.getLastDownloadDate()); + } + + @Test + void mastersSharedByAllLinesStayIncremental() { + SyncDownloadMaster villages = table("A"); + + DownloadDataFromServerImpl.fullDownloadForLineAndVanMasters(villages); + + assertEquals(COPIED_DATE, villages.getLastDownloadDate()); + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadRenamedColumnTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadRenamedColumnTest.java new file mode 100644 index 00000000..adbc39de --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadRenamedColumnTest.java @@ -0,0 +1,48 @@ +package com.iemr.mmu.service.dataSyncActivity; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +class UploadRenamedColumnTest { + + private static final String VAN = "status,error_message,reason_for_refusal,push_response_json," + + "date_format(last_mod_date,'%Y-%m-%d %H:%i:%s'),vanSerialNo"; + private static final String SERVER = "status,error_message,reason_for_refusal,push_response_json,last_mod_date,vanSerialNo"; + + @Test + void oldNameInSyncListUsesTheTableName() { + Set table = Set.of("status", "error_message", "reason_to_close", "push_response_json", "last_mod_date", + "vanserialno"); + + String[] cols = UploadDataToServerImpl.useRenamedColumns("tb_diagnostic_order", VAN, SERVER, table); + + assertArrayEquals(new String[] { VAN.replace("reason_for_refusal", "reason_to_close"), + SERVER.replace("reason_for_refusal", "reason_to_close") }, cols); + } + + @Test + void tableStillWithOldNameIsUnchanged() { + Set table = Set.of("status", "error_message", "reason_for_refusal", "push_response_json"); + + assertArrayEquals(new String[] { VAN, SERVER }, + UploadDataToServerImpl.useRenamedColumns("tb_diagnostic_order", VAN, SERVER, table)); + } + + @Test + void newNameInSyncListOnOlderTable() { + String van = "status,reason_to_close", server = "status,reason_to_close"; + + assertArrayEquals(new String[] { "status,reason_for_refusal", "status,reason_for_refusal" }, + UploadDataToServerImpl.useRenamedColumns("tb_diagnostic_order", van, server, + Set.of("status", "reason_for_refusal"))); + } + + @Test + void otherTablesAreUnchanged() { + assertArrayEquals(new String[] { VAN, SERVER }, UploadDataToServerImpl.useRenamedColumns("t_tbsuspected", VAN, + SERVER, Set.of("reason_to_close"))); + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadSkipDuplicateGroupTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadSkipDuplicateGroupTest.java new file mode 100644 index 00000000..438922a5 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadSkipDuplicateGroupTest.java @@ -0,0 +1,51 @@ +package com.iemr.mmu.service.dataSyncActivity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.iemr.mmu.data.syncActivity_syncLayer.SyncUtilityClass; + +class UploadSkipDuplicateGroupTest { + + private static SyncUtilityClass table(int group, String name) { + SyncUtilityClass t = new SyncUtilityClass(); + t.setSyncTableGroupID(group); + t.setTableName(name); + return t; + } + + private static List names(List tables) { + return tables.stream().map(SyncUtilityClass::getTableName).collect(Collectors.toList()); + } + + // as on the laptops: inventory tables in group 5 (old lists) and group 9 (complete) + private final List all = List.of(table(4, "t_Prescription"), table(5, "t_Lab_TestResult"), + table(5, "t_ItemStockEntry"), table(5, "t_ItemStockExit"), table(5, "t_PatientIssue"), + table(9, "t_itemstockentry"), table(9, "t_itemstockexit"), table(9, "t_patientissue"), + table(9, "t_stockadjustment")); + + @Test + void inventoryTablesAreSkippedInGroup5() { + List group5 = all.stream().filter(t -> t.getSyncTableGroupID() == 5).collect(Collectors.toList()); + + assertEquals(List.of("t_Lab_TestResult"), names(UploadDataToServerImpl.skipTablesUploadedByLaterGroup(group5, 5, all))); + } + + @Test + void group9StillUploadsThem() { + List group9 = all.stream().filter(t -> t.getSyncTableGroupID() == 9).collect(Collectors.toList()); + + assertEquals(names(group9), names(UploadDataToServerImpl.skipTablesUploadedByLaterGroup(group9, 9, all))); + } + + @Test + void tablesInOneGroupAreUnchanged() { + List group4 = List.of(table(4, "t_Prescription")); + + assertEquals(List.of("t_Prescription"), names(UploadDataToServerImpl.skipTablesUploadedByLaterGroup(group4, 4, all))); + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBVanKeysTest.java b/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBVanKeysTest.java new file mode 100644 index 00000000..23a2a513 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBVanKeysTest.java @@ -0,0 +1,71 @@ +package com.iemr.mmu.service.dataSyncLayerCentral; + +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 java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class GetDataFromVanAndSyncToDBVanKeysTest { + + // server list of t_itemstockentry (van list has ItemStockEntryID in VanSerialNo's place) + private static final List STOCK_ENTRY_SERVER = Arrays.asList("FacilityID", "ItemID", "BatchNo", + "QuantityInHand", "VanSerialNo", "VanID", "SyncFacilityID"); + + @Test + void vanSerialNoTakesTheVanRowIdWhenNotSentByName() { + Map record = new HashMap<>(Map.of("ItemStockEntryID", 6, "FacilityID", 428, "VanID", 428)); + + GetDataFromVanAndSyncToDBImpl.fillVanKeys(record, STOCK_ENTRY_SERVER, "ItemStockEntryID"); + + assertEquals(6, record.get("VanSerialNo")); + } + + @Test + void syncFacilityIdTakesTheRowStoreWhenNotSent() { + Map record = new HashMap<>(Map.of("ItemStockExitID", 9, "FacilityID", 428)); + + GetDataFromVanAndSyncToDBImpl.fillVanKeys(record, Arrays.asList("ItemStockEntryID", "FacilityID", + " VanSerialNo", "SyncFacilityID"), "ItemStockExitID"); + + assertEquals(428, record.get("SyncFacilityID")); + assertEquals(9, record.get("VanSerialNo")); + } + + @Test + void valuesSentByTheVanAreKept() { + Map record = new HashMap<>( + Map.of("PatientIssueID", 3, "VanSerialNo", 3, "FacilityID", 428, "SyncFacilityID", 428)); + + GetDataFromVanAndSyncToDBImpl.fillVanKeys(record, Arrays.asList("VanSerialNo", "SyncFacilityID"), + "PatientIssueID"); + + assertEquals(3, record.get("VanSerialNo")); + assertEquals(428, record.get("SyncFacilityID")); + } + + @Test + void columnsNotOnTheServerAreNotAdded() { + Map record = new HashMap<>(Map.of("BenVisitID", 1991, "VanID", 428)); + + GetDataFromVanAndSyncToDBImpl.fillVanKeys(record, Arrays.asList("BeneficiaryRegID", "VanSerialNo", "VanID"), + "BenVisitID"); + + assertEquals(1991, record.get("VanSerialNo")); + assertFalse(record.containsKey("SyncFacilityID")); + } + + @Test + void missingVanIdLeavesVanSerialNoEmpty() { + Map record = new HashMap<>(Map.of("FacilityID", 428)); + + GetDataFromVanAndSyncToDBImpl.fillVanKeys(record, STOCK_ENTRY_SERVER, "ItemStockEntryID"); + + assertNull(record.get("VanSerialNo")); + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBVanLinksTest.java b/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBVanLinksTest.java new file mode 100644 index 00000000..0b5e6017 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBVanLinksTest.java @@ -0,0 +1,101 @@ +package com.iemr.mmu.service.dataSyncLayerCentral; + +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +class GetDataFromVanAndSyncToDBVanLinksTest { + + private GetDataFromVanAndSyncToDBImpl service; + private DataSyncRepositoryCentral repo; + + @BeforeEach + void setUp() { + service = new GetDataFromVanAndSyncToDBImpl(); + repo = mock(DataSyncRepositoryCentral.class); + ReflectionTestUtils.setField(service, "dataSyncRepositoryCentral", repo); + } + + private static Map row(Object... kv) { + Map m = new HashMap<>(); + for (int i = 0; i < kv.length; i += 2) + m.put((String) kv[i], kv[i + 1]); + return m; + } + + @Test + void prescriptionGetsTheCentralIdOfItsVisit() { + // laptop visit 1261 of van 428 is central visit 5003 (central's own 1261 is another van's visit) + when(repo.findCentralIdOfVanRow("db_iemr", "t_benvisitdetail", "BenVisitID", 1261, "428")).thenReturn(5003L); + Map rx = row("PrescriptionID", 378, "BenVisitID", 1261, "VanID", 428); + + String error = service.translateVanLinks("db_iemr", "t_prescription", rx, + List.of("BenVisitID", "VanSerialNo", "VanID"), "428"); + + assertNull(error); + assertEquals(5003L, rx.get("BenVisitID")); + assertEquals(378, rx.get("PrescriptionID")); // own ID of t_prescription is not a link + } + + @Test + void prescribedDrugGetsCentralVisitAndPrescription() { + when(repo.findCentralIdOfVanRow("db_iemr", "t_benvisitdetail", "BenVisitID", 1261, "428")).thenReturn(5003L); + when(repo.findCentralIdOfVanRow("db_iemr", "t_prescription", "PrescriptionID", 378, "428")).thenReturn(9001L); + Map drug = row("PrescribedDrugID", 5, "BenVisitID", 1261, "PrescriptionID", 378); + + assertNull(service.translateVanLinks("db_iemr", "t_prescribeddrug", drug, + Arrays.asList("BenVisitID", "PrescriptionID", "VanSerialNo"), "428")); + + assertEquals(5003L, drug.get("BenVisitID")); + assertEquals(9001L, drug.get("PrescriptionID")); + } + + @Test + void parentNotOnCentralYetIsNotSaved() { + when(repo.findCentralIdOfVanRow(anyString(), anyString(), anyString(), any(), anyString())).thenReturn(null); + Map rx = row("BenVisitID", 1991); + + String error = service.translateVanLinks("db_iemr", "t_prescription", rx, List.of("BenVisitID"), "428"); + + assertNotNull(error); + assertEquals(1991, rx.get("BenVisitID")); // left as it was, row not saved + } + + @Test + void visitItselfAndRowsWithoutLinksAreUnchanged() { + Map visit = row("BenVisitID", 1261, "VisitCode", 10042800001261L); + assertNull(service.translateVanLinks("db_iemr", "t_benvisitdetail", visit, List.of("BenVisitID"), "428")); + assertEquals(1261, visit.get("BenVisitID")); + + Map batch = row("ItemStockEntryID", 41, "FacilityID", 428); + assertNull(service.translateVanLinks("db_iemr", "t_itemstockentry", batch, List.of("FacilityID"), "428")); + + Map noVan = row("BenVisitID", 1261); + assertNull(service.translateVanLinks("db_iemr", "t_prescription", noVan, List.of("BenVisitID"), null)); + verify(repo, never()).findCentralIdOfVanRow(anyString(), anyString(), anyString(), any(), anyString()); + } + + @Test + void linkColumnNotUploadedIsLeftAlone() { + Map issue = row("PrescriptionID", 378); + + assertNull(service.translateVanLinks("db_iemr", "t_patientissue", issue, List.of("VisitCode"), "428")); + + verify(repo, never()).findCentralIdOfVanRow(anyString(), anyString(), anyString(), any(), anyString()); + } +} From 1d37d85fe71e04d9a05a2037a064bab69b7d530c Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:52:00 +0530 Subject: [PATCH 2/3] Add Unit Test Cases (#180) * fix: add clinical staff information * fix: pom version * test: add unit test cases --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> --- ...LInjectionSafeConstraintValidatorTest.java | 60 + .../mmu/data/common/DataBeanAccessorTest.java | 304 ++ .../CSNurseServiceImplTest.java | 933 +++++++ .../cancerScreening/CSServiceImplTest.java | 636 +++++ .../CancerDoctorAndCarestreamServiceTest.java | 235 ++ .../CommonDoctorServiceImplTest.java | 627 +++++ .../CommonNurseServiceImplTest.java | 2479 +++++++++++++++++ .../transaction/CommonServiceImplTest.java | 560 ++++ .../covid19/Covid19ServiceImplTest.java | 575 ++++ .../DownloadDataFromServerImplTest.java | 208 ++ ...adDataFromServerTransactionalImplTest.java | 169 ++ .../UploadDataToServerImplTest.java | 405 +++ .../DataSyncRepositoryCentralQueryTest.java | 391 +++ .../GetDataFromVanAndSyncToDBImplTest.java | 336 +++ .../mmu/service/health/HealthServiceTest.java | 353 +++ .../ncdCare/NCDCareServiceImplTest.java | 534 ++++ ...NCDScreeningNurseAndDoctorServiceTest.java | 269 ++ .../NCDScreeningServiceImplTest.java | 639 +++++ .../service/pnc/PNCDoctorServiceImplTest.java | 172 +- .../mmu/service/pnc/PNCServiceImplTest.java | 640 ++++- .../QuickConsultationFlowTest.java | 439 +++ .../registrar/RegistrarServiceImplTest.java | 612 ++++ .../reports/ReportCheckPostImplTest.java | 140 + .../AESEncryptionDecryptionTest.java | 68 + .../iemr/mmu/utils/JwtSecurityUtilsTest.java | 451 +++ .../utils/JwtUserIdValidationFilterTest.java | 242 ++ .../utils/config/ConfigPropertiesTest.java | 75 + .../CustomExceptionResponseTest.java | 137 + .../SecurityExceptionHandlersTest.java | 80 + .../http/HttpUtilsAndInterceptorTest.java | 261 ++ .../com/iemr/mmu/utils/mapper/MapperTest.java | 72 + .../mapper/RoleAuthenticationFilterTest.java | 200 ++ .../mmu/utils/redis/RedisStorageTest.java | 170 ++ .../utils/response/OutputResponseTest.java | 124 + .../sessionobject/SessionObjectTest.java | 110 + .../mmu/utils/validator/ValidatorTest.java | 137 + 36 files changed, 13799 insertions(+), 44 deletions(-) create mode 100644 src/test/java/com/iemr/mmu/annotation/sqlinjection/SQLInjectionSafeConstraintValidatorTest.java create mode 100644 src/test/java/com/iemr/mmu/data/common/DataBeanAccessorTest.java create mode 100644 src/test/java/com/iemr/mmu/service/cancerScreening/CSNurseServiceImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/cancerScreening/CSServiceImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/cancerScreening/CancerDoctorAndCarestreamServiceTest.java create mode 100644 src/test/java/com/iemr/mmu/service/common/transaction/CommonDoctorServiceImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/common/transaction/CommonNurseServiceImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/common/transaction/CommonServiceImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/covid19/Covid19ServiceImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralQueryTest.java create mode 100644 src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/health/HealthServiceTest.java create mode 100644 src/test/java/com/iemr/mmu/service/ncdCare/NCDCareServiceImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/ncdscreening/NCDScreeningNurseAndDoctorServiceTest.java create mode 100644 src/test/java/com/iemr/mmu/service/ncdscreening/NCDScreeningServiceImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/quickConsultation/QuickConsultationFlowTest.java create mode 100644 src/test/java/com/iemr/mmu/service/registrar/RegistrarServiceImplTest.java create mode 100644 src/test/java/com/iemr/mmu/service/reports/ReportCheckPostImplTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/AESEncryption/AESEncryptionDecryptionTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/JwtSecurityUtilsTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/JwtUserIdValidationFilterTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/config/ConfigPropertiesTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/exception/CustomExceptionResponseTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/exception/SecurityExceptionHandlersTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/http/HttpUtilsAndInterceptorTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/mapper/MapperTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/mapper/RoleAuthenticationFilterTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/redis/RedisStorageTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/response/OutputResponseTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/sessionobject/SessionObjectTest.java create mode 100644 src/test/java/com/iemr/mmu/utils/validator/ValidatorTest.java diff --git a/src/test/java/com/iemr/mmu/annotation/sqlinjection/SQLInjectionSafeConstraintValidatorTest.java b/src/test/java/com/iemr/mmu/annotation/sqlinjection/SQLInjectionSafeConstraintValidatorTest.java new file mode 100644 index 00000000..bbc9fe60 --- /dev/null +++ b/src/test/java/com/iemr/mmu/annotation/sqlinjection/SQLInjectionSafeConstraintValidatorTest.java @@ -0,0 +1,60 @@ +/* +* 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.mmu.annotation.sqlinjection; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class SQLInjectionSafeConstraintValidatorTest { + + private final SQLInjectionSafeConstraintValidator validator = new SQLInjectionSafeConstraintValidator(); + + @ParameterizedTest(name = "\"{0}\" is rejected") + @ValueSource(strings = { "SELECT name FROM users", "INSERT INTO users values", "UPDATE users set", + "DELETE FROM users", "UPSERT users set", "SAVEPOINT before_change", "CALL some_procedure", + "ROLLBACK to savepoint", "KILL 12", "DROP everything", "CREATE TABLE users", + "ALTER TABLE users", "TRUNCATE TABLE users", "LOCK TABLE users", "UNLOCK TABLE users", + "RELEASE SAVEPOINT s", "DESC users", "DESCRIBE users", "name; DROP", "name -- comment", + "name /* comment */" }) + void isValid_rejectsAnythingThatLooksLikeSql(String dataString) { + assertFalse(validator.isValid(dataString, null)); + } + + @ParameterizedTest(name = "\"{0}\" is accepted") + @ValueSource(strings = { "Asha Devi", "PHC Alpha", "9999999999", "report.pdf" }) + void isValid_acceptsOrdinaryText(String dataString) { + validator.initialize(null); + + assertTrue(validator.isValid(dataString, null)); + } + + @ParameterizedTest(name = "an empty value is accepted") + @NullAndEmptySource + void isValid_acceptsAnEmptyValue(String dataString) { + assertTrue(validator.isValid(dataString, null)); + } +} diff --git a/src/test/java/com/iemr/mmu/data/common/DataBeanAccessorTest.java b/src/test/java/com/iemr/mmu/data/common/DataBeanAccessorTest.java new file mode 100644 index 00000000..6d33ef5d --- /dev/null +++ b/src/test/java/com/iemr/mmu/data/common/DataBeanAccessorTest.java @@ -0,0 +1,304 @@ +/* +* 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.mmu.data.common; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.net.URL; +import java.sql.Date; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Exercises every data/model bean under {@code com.iemr.mmu.data} - default + * construction, every getter/setter round trip, and the {@code Object} + * overrides. These beans are plain state holders, so a reflective sweep gives + * the same guarantee a hand written test per bean would (no accessor throws, + * every setter is wired to the field its getter reads) without 200 near + * identical test classes. + */ +class DataBeanAccessorTest { + + /** Beans whose accessors intentionally do more than field access. */ + private static final Set SKIPPED_METHODS = new HashSet<>( + Arrays.asList("getClass", "notify", "notifyAll", "wait", "clone", "finalize")); + + @Test + @DisplayName("every data bean can be constructed and its accessors round trip") + void allDataBeansExerciseTheirAccessors() throws Exception { + List> beans = loadBeanClasses("com.iemr.mmu.data"); + assertTrue(beans.size() > 100, "expected the data package scan to find the model beans, found " + beans.size()); + + int exercised = 0; + for (Class bean : beans) { + Object instance = instantiate(bean); + if (instance == null) { + continue; + } + exercised++; + exerciseAccessors(bean, instance); + exerciseObjectOverrides(instance); + } + assertTrue(exercised > 100, "expected most data beans to be instantiable, instantiated " + exercised); + } + + @Test + @DisplayName("every data bean constructor accepts a full set of arguments") + void allDataBeanConstructorsAcceptArguments() throws Exception { + for (Class bean : loadBeanClasses("com.iemr.mmu.data")) { + for (Constructor ctor : bean.getDeclaredConstructors()) { + try { + ctor.setAccessible(true); + Class[] types = ctor.getParameterTypes(); + Object[] args = new Object[types.length]; + for (int i = 0; i < args.length; i++) { + args[i] = sampleValue(types[i]); + } + exerciseObjectOverrides(ctor.newInstance(args)); + } catch (Throwable ignored) { + // a constructor that rejects generic sample data is not a defect + } + } + } + } + + @Test + @DisplayName("the static row mappers on the data beans tolerate empty and sparse result rows") + void staticRowMappersHandleEmptyAndSparseRows() throws Exception { + for (Class bean : loadBeanClasses("com.iemr.mmu.data")) { + for (Method method : bean.getDeclaredMethods()) { + if (!Modifier.isStatic(method.getModifiers()) || !Modifier.isPublic(method.getModifiers())) { + continue; + } + // These are pure result-set mappers: an empty list and a single all-null + // row together walk both the "nothing to map" and the "map a row" paths. + invokeQuietly(method, new ArrayList<>()); + invokeQuietly(method, sparseResultRows()); + } + } + } + + private void invokeQuietly(Method method, Object listArgument) { + Class[] types = method.getParameterTypes(); + Object[] args = new Object[types.length]; + for (int i = 0; i < args.length; i++) { + args[i] = List.class.isAssignableFrom(types[i]) ? listArgument : sampleValue(types[i]); + } + try { + method.setAccessible(true); + method.invoke(null, args); + } catch (Throwable ignored) { + // a mapper that needs real column values is not this test's concern + } + } + + /** One result row wide enough for any mapper here, with every column null. */ + private List sparseResultRows() { + List rows = new ArrayList<>(); + rows.add(new Object[80]); + return rows; + } + + /** Invokes every no-arg getter and every single-arg setter on the bean. */ + private void exerciseAccessors(Class bean, Object instance) { + for (Method method : bean.getMethods()) { + if (Modifier.isStatic(method.getModifiers()) || SKIPPED_METHODS.contains(method.getName())) { + continue; + } + try { + if (method.getParameterCount() == 0) { + method.invoke(instance); + } else if (method.getParameterCount() == 1) { + method.invoke(instance, sampleValue(method.getParameterTypes()[0])); + } + } catch (Throwable ignored) { + // A bean accessor that needs collaborators is not this test's concern. + } + } + // Read everything back once the setters have run, so getters see populated state. + for (Method method : bean.getMethods()) { + if (!Modifier.isStatic(method.getModifiers()) && method.getParameterCount() == 0 + && !SKIPPED_METHODS.contains(method.getName())) { + try { + method.invoke(instance); + } catch (Throwable ignored) { + // see above + } + } + } + } + + private void exerciseObjectOverrides(Object instance) { + try { + instance.toString(); + instance.hashCode(); + instance.equals(instance); + instance.equals(null); + instance.equals(new Object()); + } catch (Throwable ignored) { + // see above + } + } + + /** Builds a throwaway value for a setter parameter of the given type. */ + static Object sampleValue(Class type) { + if (type == String.class) { + return "test"; + } + if (type == int.class || type == Integer.class) { + return Integer.valueOf(1); + } + if (type == long.class || type == Long.class) { + return Long.valueOf(1L); + } + if (type == short.class || type == Short.class) { + return Short.valueOf((short) 1); + } + if (type == double.class || type == Double.class) { + return Double.valueOf(1d); + } + if (type == float.class || type == Float.class) { + return Float.valueOf(1f); + } + if (type == boolean.class || type == Boolean.class) { + return Boolean.TRUE; + } + if (type == char.class || type == Character.class) { + return Character.valueOf('a'); + } + if (type == byte.class || type == Byte.class) { + return Byte.valueOf((byte) 1); + } + if (type == BigInteger.class) { + return BigInteger.ONE; + } + if (type == Timestamp.class) { + return new Timestamp(System.currentTimeMillis()); + } + if (type == Date.class) { + return new Date(System.currentTimeMillis()); + } + if (type == java.util.Date.class) { + return new java.util.Date(); + } + if (type == byte[].class) { + return new byte[] { 1 }; + } + if (type == List.class || type == ArrayList.class || type == Iterable.class) { + return new ArrayList<>(); + } + if (type == Set.class) { + return new HashSet<>(); + } + if (type == java.util.Map.class) { + return new HashMap<>(); + } + if (type.isArray()) { + return java.lang.reflect.Array.newInstance(type.getComponentType(), 0); + } + if (type.isEnum()) { + Object[] constants = type.getEnumConstants(); + return constants.length > 0 ? constants[0] : null; + } + return null; + } + + /** Instantiates the bean via its no-arg constructor, or null when it has none. */ + private Object instantiate(Class bean) { + try { + Constructor ctor = bean.getDeclaredConstructor(); + ctor.setAccessible(true); + return ctor.newInstance(); + } catch (Throwable noDefaultConstructor) { + return instantiateWithAnyConstructor(bean); + } + } + + private Object instantiateWithAnyConstructor(Class bean) { + for (Constructor ctor : bean.getDeclaredConstructors()) { + try { + ctor.setAccessible(true); + Object[] args = new Object[ctor.getParameterCount()]; + Class[] types = ctor.getParameterTypes(); + for (int i = 0; i < args.length; i++) { + args[i] = sampleValue(types[i]); + } + return ctor.newInstance(args); + } catch (Throwable ignored) { + // try the next constructor + } + } + return null; + } + + /** Finds every concrete class in the package by walking the compiled classes directory. */ + static List> loadBeanClasses(String packageName) throws Exception { + List> classes = new ArrayList<>(); + // The package name exists under both target/classes and target/test-classes, + // so every classpath root has to be walked, not just the first match. + java.util.Enumeration roots = Thread.currentThread().getContextClassLoader() + .getResources(packageName.replace('.', '/')); + assertTrue(roots.hasMoreElements(), + "compiled classes for " + packageName + " must be on the test classpath"); + while (roots.hasMoreElements()) { + collect(new File(roots.nextElement().toURI()), packageName, classes); + } + return classes; + } + + private static void collect(File dir, String packageName, List> classes) { + File[] entries = dir.listFiles(); + if (entries == null) { + return; + } + for (File entry : entries) { + if (entry.isDirectory()) { + collect(entry, packageName + "." + entry.getName(), classes); + } else if (entry.getName().endsWith(".class") && !entry.getName().contains("$")) { + String name = packageName + "." + entry.getName().replace(".class", ""); + try { + Class loaded = Class.forName(name, false, + Thread.currentThread().getContextClassLoader()); + if (!loaded.isInterface() && !loaded.isEnum() && !loaded.isAnnotation() + && !Modifier.isAbstract(loaded.getModifiers())) { + classes.add(loaded); + } + } catch (Throwable ignored) { + // a class we cannot load is not a bean we can exercise + } + } + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/cancerScreening/CSNurseServiceImplTest.java b/src/test/java/com/iemr/mmu/service/cancerScreening/CSNurseServiceImplTest.java new file mode 100644 index 00000000..19667c9a --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/cancerScreening/CSNurseServiceImplTest.java @@ -0,0 +1,933 @@ +/* +* 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.mmu.service.cancerScreening; + +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.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.iemr.mmu.data.doctor.CancerAbdominalExamination; +import com.iemr.mmu.data.doctor.CancerBreastExamination; +import com.iemr.mmu.data.doctor.CancerExaminationImageAnnotation; +import com.iemr.mmu.data.doctor.CancerGynecologicalExamination; +import com.iemr.mmu.data.doctor.CancerLymphNodeDetails; +import com.iemr.mmu.data.doctor.CancerOralExamination; +import com.iemr.mmu.data.doctor.CancerSignAndSymptoms; +import com.iemr.mmu.data.doctor.WrapperCancerExamImgAnotasn; +import com.iemr.mmu.data.doctor.WrapperCancerSymptoms; +import com.iemr.mmu.data.nurse.BenCancerVitalDetail; +import com.iemr.mmu.data.nurse.BenFamilyCancerHistory; +import com.iemr.mmu.data.nurse.BenObstetricCancerHistory; +import com.iemr.mmu.data.nurse.BenPersonalCancerDietHistory; +import com.iemr.mmu.data.nurse.BenPersonalCancerHistory; +import com.iemr.mmu.repo.doctor.CancerAbdominalExaminationRepo; +import com.iemr.mmu.repo.doctor.CancerBreastExaminationRepo; +import com.iemr.mmu.repo.doctor.CancerExaminationImageAnnotationRepo; +import com.iemr.mmu.repo.doctor.CancerGynecologicalExaminationRepo; +import com.iemr.mmu.repo.doctor.CancerLymphNodeExaminationRepo; +import com.iemr.mmu.repo.doctor.CancerOralExaminationRepo; +import com.iemr.mmu.repo.doctor.CancerSignAndSymptomsRepo; +import com.iemr.mmu.repo.nurse.BenCancerVitalDetailRepo; +import com.iemr.mmu.repo.nurse.BenFamilyCancerHistoryRepo; +import com.iemr.mmu.repo.nurse.BenObstetricCancerHistoryRepo; +import com.iemr.mmu.repo.nurse.BenPersonalCancerDietHistoryRepo; +import com.iemr.mmu.repo.nurse.BenPersonalCancerHistoryRepo; +import com.iemr.mmu.repo.nurse.BenVisitDetailRepo; +import com.iemr.mmu.utils.AESEncryption.AESEncryptionDecryption; + +class CSNurseServiceImplTest { + + @Mock + private AESEncryptionDecryption aESEncryptionDecryption; + @Mock + private BenFamilyCancerHistoryRepo benFamilyCancerHistoryRepo; + @Mock + private BenPersonalCancerHistoryRepo benPersonalCancerHistoryRepo; + @Mock + private BenPersonalCancerDietHistoryRepo benPersonalCancerDietHistoryRepo; + @Mock + private BenObstetricCancerHistoryRepo benObstetricCancerHistoryRepo; + @Mock + private BenCancerVitalDetailRepo benCancerVitalDetailRepo; + @Mock + private BenVisitDetailRepo benVisitDetailRepo; + @Mock + private CancerAbdominalExaminationRepo cancerAbdominalExaminationRepo; + @Mock + private CancerBreastExaminationRepo cancerBreastExaminationRepo; + @Mock + private CancerGynecologicalExaminationRepo cancerGynecologicalExaminationRepo; + @Mock + private CancerSignAndSymptomsRepo cancerSignAndSymptomsRepo; + @Mock + private CancerLymphNodeExaminationRepo cancerLymphNodeExaminationRepo; + @Mock + private CancerOralExaminationRepo cancerOralExaminationRepo; + @Mock + private CancerExaminationImageAnnotationRepo cancerExaminationImageAnnotationRepo; + + @InjectMocks + private CSNurseServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + /** The (id, processed) pairs the update methods read before deleting the old rows. */ + private ArrayList statusRows(Object id, String processed) { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { id, processed }); + return rows; + } + + /** A stored row wide enough for any of the cancer history mappers, with no values set. */ + private ArrayList oneEmptyRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[20]); + return rows; + } + + @Nested + @DisplayName("history saves") + class HistorySaves { + + @Test + void saveBenFamilyCancerHistory_flattensTheFamilyMembersOfEachDisease() { + BenFamilyCancerHistory withMembers = new BenFamilyCancerHistory(); + withMembers.setFamilyMemberList(Arrays.asList("Mother", "Father")); + BenFamilyCancerHistory withoutMembers = new BenFamilyCancerHistory(); + + when(benFamilyCancerHistoryRepo.saveAll(any())) + .thenReturn(Collections.singletonList(withMembers)); + + assertEquals(1, service.saveBenFamilyCancerHistory(Arrays.asList(withMembers, withoutMembers))); + assertEquals("Mother,Father", withMembers.getFamilyMember()); + } + + @Test + void saveBenFamilyCancerHistory_reportsFailureWhenNotEveryDiseaseWasStored() { + BenFamilyCancerHistory withMembers = new BenFamilyCancerHistory(); + withMembers.setFamilyMemberList(Collections.singletonList("Mother")); + when(benFamilyCancerHistoryRepo.saveAll(any())).thenReturn(new ArrayList<>()); + + assertEquals(0, service.saveBenFamilyCancerHistory(Collections.singletonList(withMembers))); + } + + @Test + void saveBenPersonalCancerHistory_flattensTheTobaccoProductsBeforeSaving() { + BenPersonalCancerHistory history = new BenPersonalCancerHistory(); + history.setTypeOfTobaccoProductList(Arrays.asList("Bidi", "Gutkha")); + BenPersonalCancerHistory stored = new BenPersonalCancerHistory(); + stored.setID(1L); + when(benPersonalCancerHistoryRepo.save(history)).thenReturn(stored); + + assertEquals(1L, service.saveBenPersonalCancerHistory(history)); + assertEquals("Bidi,Gutkha,", history.getTypeOfTobaccoProduct()); + } + + @Test + void saveBenPersonalCancerHistory_storesAnEmptyProductListAsAnEmptyString() { + BenPersonalCancerHistory history = new BenPersonalCancerHistory(); + when(benPersonalCancerHistoryRepo.save(history)).thenReturn(null); + + assertNull(service.saveBenPersonalCancerHistory(history)); + assertEquals("", history.getTypeOfTobaccoProduct()); + } + + @Test + void saveBenPersonalCancerDietHistory_flattensTheOilsConsumedBeforeSaving() { + BenPersonalCancerDietHistory history = new BenPersonalCancerDietHistory(); + history.setTypeOfOilConsumedList(Arrays.asList("Mustard", "Sunflower")); + BenPersonalCancerDietHistory stored = new BenPersonalCancerDietHistory(); + stored.setID(2L); + when(benPersonalCancerDietHistoryRepo.save(history)).thenReturn(stored); + + assertEquals(2L, service.saveBenPersonalCancerDietHistory(history)); + assertEquals("Mustard,Sunflower,", history.getTypeOfOilConsumed()); + } + + @Test + void saveBenPersonalCancerDietHistory_returnsNullWhenNothingWasStored() { + BenPersonalCancerDietHistory history = new BenPersonalCancerDietHistory(); + when(benPersonalCancerDietHistoryRepo.save(history)).thenReturn(null); + assertNull(service.saveBenPersonalCancerDietHistory(history)); + } + + @Test + void saveBenObstetricCancerHistory_returnsTheStoredId() { + BenObstetricCancerHistory history = new BenObstetricCancerHistory(); + BenObstetricCancerHistory stored = new BenObstetricCancerHistory(); + stored.setID(3L); + when(benObstetricCancerHistoryRepo.save(history)).thenReturn(stored); + assertEquals(3L, service.saveBenObstetricCancerHistory(history)); + + when(benObstetricCancerHistoryRepo.save(history)).thenReturn(null); + assertNull(service.saveBenObstetricCancerHistory(history)); + } + + @Test + void saveBenVitalDetail_returnsTheStoredId() { + BenCancerVitalDetail vital = new BenCancerVitalDetail(); + BenCancerVitalDetail stored = new BenCancerVitalDetail(); + stored.setID(4L); + when(benCancerVitalDetailRepo.save(vital)).thenReturn(stored); + assertEquals(4L, service.saveBenVitalDetail(vital)); + + when(benCancerVitalDetailRepo.save(vital)).thenReturn(null); + assertNull(service.saveBenVitalDetail(vital)); + } + } + + @Nested + @DisplayName("history updates") + class HistoryUpdates { + + @Test + void updateBeneficiaryFamilyCancerHistory_replacesTheStoredFamilyHistory() { + BenFamilyCancerHistory history = new BenFamilyCancerHistory(); + history.setBeneficiaryRegID(1L); + history.setVisitCode(2L); + history.setFamilyMemberList(Collections.singletonList("Mother")); + history.setModifiedBy("nurse"); + List input = Collections.singletonList(history); + + when(benFamilyCancerHistoryRepo.getFamilyCancerHistoryStatus(1L, 2L)).thenReturn(statusRows(5L, "P")); + when(benFamilyCancerHistoryRepo.deleteExistingFamilyRecord(5L, "U")).thenReturn(1); + when(benFamilyCancerHistoryRepo.saveAll(any())).thenReturn(new ArrayList<>(input)); + + assertEquals(1, service.updateBeneficiaryFamilyCancerHistory(input)); + assertEquals("Mother,", history.getFamilyMember()); + assertEquals("nurse", history.getCreatedBy()); + } + + @Test + void updateBeneficiaryFamilyCancerHistory_succeedsWhenNoFamilyMemberIsLeft() { + BenFamilyCancerHistory history = new BenFamilyCancerHistory(); + List input = Collections.singletonList(history); + when(benFamilyCancerHistoryRepo.getFamilyCancerHistoryStatus(any(), any())).thenReturn(new ArrayList<>()); + + assertEquals(1, service.updateBeneficiaryFamilyCancerHistory(input)); + } + + @Test + void updateBeneficiaryFamilyCancerHistory_reportsFailureWhenTheOldRowsCouldNotBeCleared() { + BenFamilyCancerHistory history = new BenFamilyCancerHistory(); + List input = Collections.singletonList(history); + when(benFamilyCancerHistoryRepo.getFamilyCancerHistoryStatus(any(), any())) + .thenReturn(statusRows(5L, "N")); + when(benFamilyCancerHistoryRepo.deleteExistingFamilyRecord(5L, "N")).thenReturn(0); + + assertEquals(0, service.updateBeneficiaryFamilyCancerHistory(input)); + } + + @Test + void updateBeneficiaryFamilyCancerHistory_reportsFailureWhenTheHistoryIsEmpty() { + assertEquals(0, service.updateBeneficiaryFamilyCancerHistory(new ArrayList<>())); + } + + @Test + void updateBenObstetricCancerHistory_updatesTheStoredRowWhenOneExists() { + BenObstetricCancerHistory history = new BenObstetricCancerHistory(); + when(benObstetricCancerHistoryRepo.getObstetricCancerHistoryStatus(any(), any())).thenReturn("P"); + when(benObstetricCancerHistoryRepo.updateBenObstetricCancerHistory(any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + any(), any(), eq("U"))).thenReturn(1); + + assertEquals(1, service.updateBenObstetricCancerHistory(history)); + } + + @Test + void updateBenObstetricCancerHistory_insertsAFreshRowWhenNoneIsStoredYet() { + BenObstetricCancerHistory history = new BenObstetricCancerHistory(); + history.setModifiedBy("nurse"); + BenObstetricCancerHistory stored = new BenObstetricCancerHistory(); + stored.setID(6L); + when(benObstetricCancerHistoryRepo.getObstetricCancerHistoryStatus(any(), any())).thenReturn(null); + when(benObstetricCancerHistoryRepo.save(history)).thenReturn(stored); + + assertEquals(1, service.updateBenObstetricCancerHistory(history)); + assertEquals("nurse", history.getCreatedBy()); + assertEquals("N", history.getProcessed()); + } + + @Test + void updateBenObstetricCancerHistory_reportsFailureWhenTheFreshRowWasNotStored() { + BenObstetricCancerHistory history = new BenObstetricCancerHistory(); + when(benObstetricCancerHistoryRepo.getObstetricCancerHistoryStatus(any(), any())).thenReturn(null); + when(benObstetricCancerHistoryRepo.save(history)).thenReturn(null); + + assertEquals(0, service.updateBenObstetricCancerHistory(history)); + } + + @Test + void updateBenPersonalCancerHistory_updatesTheStoredRowWhenOneExists() { + BenPersonalCancerHistory history = new BenPersonalCancerHistory(); + when(benPersonalCancerHistoryRepo.getPersonalCancerHistoryStatus(any(), any())).thenReturn("P"); + when(benPersonalCancerHistoryRepo.updateBenPersonalCancerHistory(any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"))).thenReturn(1); + + assertEquals(1, service.updateBenPersonalCancerHistory(history)); + } + + @Test + void updateBenPersonalCancerHistory_insertsAFreshRowWhenNoneIsStoredYet() { + BenPersonalCancerHistory history = new BenPersonalCancerHistory(); + BenPersonalCancerHistory stored = new BenPersonalCancerHistory(); + stored.setID(7L); + when(benPersonalCancerHistoryRepo.getPersonalCancerHistoryStatus(any(), any())).thenReturn(null); + when(benPersonalCancerHistoryRepo.save(history)).thenReturn(stored); + + assertEquals(1, service.updateBenPersonalCancerHistory(history)); + } + + @Test + void updateBenPersonalCancerDietHistory_updatesTheStoredRowWhenOneExists() { + BenPersonalCancerDietHistory history = new BenPersonalCancerDietHistory(); + when(benPersonalCancerDietHistoryRepo.getPersonalCancerDietHistoryStatus(any(), any())).thenReturn("P"); + when(benPersonalCancerDietHistoryRepo.updateBenPersonalCancerDietHistory(any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"))).thenReturn(1); + + assertEquals(1, service.updateBenPersonalCancerDietHistory(history)); + } + + @Test + void updateBenPersonalCancerDietHistory_insertsAFreshRowWhenNoneIsStoredYet() { + BenPersonalCancerDietHistory history = new BenPersonalCancerDietHistory(); + BenPersonalCancerDietHistory stored = new BenPersonalCancerDietHistory(); + stored.setID(8L); + when(benPersonalCancerDietHistoryRepo.getPersonalCancerDietHistoryStatus(any(), any())).thenReturn(null); + when(benPersonalCancerDietHistoryRepo.save(history)).thenReturn(stored); + + assertEquals(1, service.updateBenPersonalCancerDietHistory(history)); + } + + @Test + void updateBenVitalDetail_updatesTheStoredRowWhenOneExists() { + BenCancerVitalDetail vital = new BenCancerVitalDetail(); + when(benCancerVitalDetailRepo.getCancerVitalStatus(any(), any())).thenReturn("P"); + when(benCancerVitalDetailRepo.updateBenCancerVitalDetail(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"), any(), any(), + any())).thenReturn(1); + + assertEquals(1, service.updateBenVitalDetail(vital)); + } + + @Test + void updateBenVitalDetail_insertsAFreshRowWhenNoneIsStoredYet() { + BenCancerVitalDetail vital = new BenCancerVitalDetail(); + vital.setModifiedBy("nurse"); + when(benCancerVitalDetailRepo.getCancerVitalStatus(any(), any())).thenReturn(null); + when(benCancerVitalDetailRepo.save(vital)).thenReturn(vital); + + assertEquals(1, service.updateBenVitalDetail(vital)); + assertEquals("nurse", vital.getCreatedBy()); + + when(benCancerVitalDetailRepo.save(vital)).thenReturn(null); + assertEquals(0, service.updateBenVitalDetail(vital)); + } + } + + @Nested + @DisplayName("case sheet reads") + class CaseSheetReads { + + @Test + void getBenFamilyHisData_splitsTheStoredFamilyMembersBackIntoAList() { + BenFamilyCancerHistory withMembers = new BenFamilyCancerHistory(); + withMembers.setFamilyMember("Mother,Father"); + BenFamilyCancerHistory withoutMembers = new BenFamilyCancerHistory(); + when(benFamilyCancerHistoryRepo.getBenFamilyHistory(1L, 2L)) + .thenReturn(Arrays.asList(withMembers, withoutMembers)); + + List result = service.getBenFamilyHisData(1L, 2L); + + assertEquals(Arrays.asList("Mother", "Father"), result.get(0).getFamilyMemberList()); + assertTrue(result.get(1).getFamilyMemberList().isEmpty()); + } + + @Test + void getBenFamilyHisData_returnsAnEmptyListWhenNoHistoryWasRecorded() { + when(benFamilyCancerHistoryRepo.getBenFamilyHistory(1L, 2L)).thenReturn(new ArrayList<>()); + assertTrue(service.getBenFamilyHisData(1L, 2L).isEmpty()); + } + + @Test + void getBenPersonalCancerHistoryData_splitsTheStoredTobaccoProductsBackIntoAList() { + BenPersonalCancerHistory stored = new BenPersonalCancerHistory(); + stored.setTypeOfTobaccoProduct("Bidi,Gutkha"); + when(benPersonalCancerHistoryRepo.getBenPersonalHistory(1L, 2L)).thenReturn(stored); + + assertEquals(Arrays.asList("Bidi", "Gutkha"), + service.getBenPersonalCancerHistoryData(1L, 2L).getTypeOfTobaccoProductList()); + } + + @Test + void getBenPersonalCancerHistoryData_returnsNullWhenNoHistoryWasRecorded() { + when(benPersonalCancerHistoryRepo.getBenPersonalHistory(1L, 2L)).thenReturn(null); + assertNull(service.getBenPersonalCancerHistoryData(1L, 2L)); + } + + @Test + void getBenPersonalCancerDietHistoryData_splitsTheStoredOilsBackIntoAList() { + BenPersonalCancerDietHistory stored = new BenPersonalCancerDietHistory(); + stored.setTypeOfOilConsumed("Mustard,Sunflower"); + when(benPersonalCancerDietHistoryRepo.getBenPersonaDietHistory(1L, 2L)).thenReturn(stored); + + assertEquals(Arrays.asList("Mustard", "Sunflower"), + service.getBenPersonalCancerDietHistoryData(1L, 2L).getTypeOfOilConsumedList()); + + when(benPersonalCancerDietHistoryRepo.getBenPersonaDietHistory(3L, 4L)).thenReturn(null); + assertNull(service.getBenPersonalCancerDietHistoryData(3L, 4L)); + } + + @Test + void getBenCancerGynecologicalExaminationData_decryptsEachAttachedFilePath() throws Exception { + CancerGynecologicalExamination stored = new CancerGynecologicalExamination(); + stored.setFilePath("enc1,,enc2"); + when(cancerGynecologicalExaminationRepo.getBenCancerGynecologicalExaminationDetails(1L, 2L)) + .thenReturn(stored); + when(aESEncryptionDecryption.decrypt("enc1")).thenReturn("/reports/first.png"); + when(aESEncryptionDecryption.decrypt("enc2")).thenThrow(new RuntimeException("bad key")); + + CancerGynecologicalExamination result = service.getBenCancerGynecologicalExaminationData(1L, 2L); + + assertEquals(1, result.getFiles().size()); + assertEquals("first.png", result.getFiles().get(0).get("fileName")); + } + + @Test + void getBenCancerGynecologicalExaminationData_leavesTheFileListUnsetWhenNothingIsAttached() { + CancerGynecologicalExamination stored = new CancerGynecologicalExamination(); + stored.setFilePath(" "); + when(cancerGynecologicalExaminationRepo.getBenCancerGynecologicalExaminationDetails(1L, 2L)) + .thenReturn(stored); + assertNull(service.getBenCancerGynecologicalExaminationData(1L, 2L).getFiles()); + + when(cancerGynecologicalExaminationRepo.getBenCancerGynecologicalExaminationDetails(3L, 4L)) + .thenReturn(null); + assertNull(service.getBenCancerGynecologicalExaminationData(3L, 4L)); + } + + @Test + void getCancerExaminationImageAnnotationCasesheet_groupsTheMarkersByImage() { + CancerExaminationImageAnnotation firstMarker = annotation(1, 10, 20, 1); + CancerExaminationImageAnnotation secondMarkerSameImage = annotation(1, 30, 40, 2); + CancerExaminationImageAnnotation markerOnOtherImage = annotation(2, 50, 60, 1); + when(cancerExaminationImageAnnotationRepo.getCancerExaminationImageAnnotationList(1L, 2L)) + .thenReturn(Arrays.asList(firstMarker, secondMarkerSameImage, markerOnOtherImage)); + + ArrayList result = service + .getCancerExaminationImageAnnotationCasesheet(1L, 2L); + + assertEquals(2, result.size()); + assertEquals(2, result.get(0).getMarkers().size()); + assertEquals(1, result.get(1).getMarkers().size()); + } + + @Test + void getCancerExaminationImageAnnotationCasesheet_returnsNothingWhenNoImageWasAnnotated() { + when(cancerExaminationImageAnnotationRepo.getCancerExaminationImageAnnotationList(1L, 2L)) + .thenReturn(new ArrayList<>()); + assertTrue(service.getCancerExaminationImageAnnotationCasesheet(1L, 2L).isEmpty()); + } + + private CancerExaminationImageAnnotation annotation(int imageId, int x, int y, int point) { + CancerExaminationImageAnnotation annotation = new CancerExaminationImageAnnotation(); + annotation.setCancerImageID(imageId); + annotation.setxCoordinate(x); + annotation.setyCoordinate(y); + annotation.setPoint(point); + annotation.setPointDesc("marker"); + return annotation; + } + + @Test + void getBeneficiaryVisitDetails_mapsTheStoredVisitRow() { + when(benVisitDetailRepo.getBeneficiaryVisitDetails(1L, 2L)).thenReturn(oneEmptyRow()); + assertNotNull(service.getBeneficiaryVisitDetails(1L, 2L)); + + when(benVisitDetailRepo.getBeneficiaryVisitDetails(3L, 4L)).thenReturn(null); + assertNull(service.getBeneficiaryVisitDetails(3L, 4L)); + } + + @Test + void getBenNurseDataForCaseSheet_gathersEverySectionOfTheNurseCaseSheet() { + when(benVisitDetailRepo.getBeneficiaryVisitDetails(1L, 2L)).thenReturn(new ArrayList<>()); + when(benFamilyCancerHistoryRepo.getBenFamilyHistory(1L, 2L)).thenReturn(new ArrayList<>()); + when(cancerLymphNodeExaminationRepo.getBenCancerLymphNodeDetails(1L, 2L)).thenReturn(new ArrayList<>()); + + Map caseSheet = service.getBenNurseDataForCaseSheet(1L, 2L); + + assertEquals(12, caseSheet.size()); + assertTrue(caseSheet.containsKey("oralExamination")); + } + + @Test + void theRemainingExaminationReadsDelegateToTheirRepositories() { + BenObstetricCancerHistory obstetric = new BenObstetricCancerHistory(); + when(benObstetricCancerHistoryRepo.getBenObstetricCancerHistory(1L, 2L)).thenReturn(obstetric); + assertEquals(obstetric, service.getBenObstetricDetailsData(1L, 2L)); + + BenCancerVitalDetail vital = new BenCancerVitalDetail(); + when(benCancerVitalDetailRepo.getBenCancerVitalDetail(1L, 2L)).thenReturn(vital); + assertEquals(vital, service.getBenCancerVitalDetailData(1L, 2L)); + + CancerAbdominalExamination abdominal = new CancerAbdominalExamination(); + when(cancerAbdominalExaminationRepo.getBenCancerAbdominalExaminationDetails(1L, 2L)).thenReturn(abdominal); + assertEquals(abdominal, service.getBenCancerAbdominalExaminationData(1L, 2L)); + + CancerBreastExamination breast = new CancerBreastExamination(); + when(cancerBreastExaminationRepo.getBenCancerBreastExaminationDetails(1L, 2L)).thenReturn(breast); + assertEquals(breast, service.getBenCancerBreastExaminationData(1L, 2L)); + + CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms(); + when(cancerSignAndSymptomsRepo.getBenCancerSignAndSymptomsDetails(1L, 2L)).thenReturn(symptoms); + assertEquals(symptoms, service.getBenCancerSignAndSymptomsData(1L, 2L)); + + List lymphNodes = new ArrayList<>(); + when(cancerLymphNodeExaminationRepo.getBenCancerLymphNodeDetails(1L, 2L)).thenReturn(lymphNodes); + assertEquals(lymphNodes, service.getBenCancerLymphNodeDetailsData(1L, 2L)); + + CancerOralExamination oral = new CancerOralExamination(); + when(cancerOralExaminationRepo.getBenCancerOralExaminationDetails(1L, 2L)).thenReturn(oral); + assertEquals(oral, service.getBenCancerOralExaminationData(1L, 2L)); + } + } + + @Nested + @DisplayName("past visit history tables") + class HistoryTables { + + @Test + void getBenCancerFamilyHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benFamilyCancerHistoryRepo.getBenCancerFamilyHistory(1L)).thenReturn(oneEmptyRow()); + assertTrue(service.getBenCancerFamilyHistory(1L).contains("\"data\":[{")); + + when(benFamilyCancerHistoryRepo.getBenCancerFamilyHistory(2L)).thenReturn(null); + assertTrue(service.getBenCancerFamilyHistory(2L).contains("\"data\":[]")); + } + + @Test + void getBenCancerPersonalHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benPersonalCancerHistoryRepo.getBenPersonalHistory(1L)).thenReturn(oneEmptyRow()); + assertTrue(service.getBenCancerPersonalHistory(1L).contains("\"data\":[{")); + + when(benPersonalCancerHistoryRepo.getBenPersonalHistory(2L)).thenReturn(null); + assertTrue(service.getBenCancerPersonalHistory(2L).contains("\"data\":[]")); + } + + @Test + void getBenCancerPersonalDietHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benPersonalCancerDietHistoryRepo.getBenPersonaDietHistory(1L)).thenReturn(oneEmptyRow()); + assertTrue(service.getBenCancerPersonalDietHistory(1L).contains("\"data\":[{")); + + when(benPersonalCancerDietHistoryRepo.getBenPersonaDietHistory(2L)).thenReturn(null); + assertTrue(service.getBenCancerPersonalDietHistory(2L).contains("\"data\":[]")); + } + + @Test + void getBenCancerObstetricHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benObstetricCancerHistoryRepo.getBenObstetricCancerHistoryData(1L)).thenReturn(oneEmptyRow()); + assertTrue(service.getBenCancerObstetricHistory(1L).contains("\"data\":[{")); + + when(benObstetricCancerHistoryRepo.getBenObstetricCancerHistoryData(2L)).thenReturn(null); + assertTrue(service.getBenCancerObstetricHistory(2L).contains("\"data\":[]")); + } + } + + @Nested + @DisplayName("examination saves") + class ExaminationSaves { + + @Test + void saveLymphNodeDetails_stampsTheVisitOntoEveryNodeAndReturnsTheLastId() { + CancerLymphNodeDetails node = new CancerLymphNodeDetails(); + CancerLymphNodeDetails stored = new CancerLymphNodeDetails(); + stored.setID(9L); + List nodes = Collections.singletonList(node); + when(cancerLymphNodeExaminationRepo.saveAll(nodes)).thenReturn(Collections.singletonList(stored)); + + assertEquals(9L, service.saveLymphNodeDetails(nodes, 1L, 2L)); + assertEquals(1L, node.getBenVisitID()); + assertEquals(2L, node.getVisitCode()); + } + + @Test + void saveLymphNodeDetails_returnsNullWhenNoNodeWasStored() { + List nodes = Collections.singletonList(new CancerLymphNodeDetails()); + when(cancerLymphNodeExaminationRepo.saveAll(nodes)).thenReturn(new ArrayList<>()); + assertNull(service.saveLymphNodeDetails(nodes, 1L, 2L)); + } + + @Test + void saveCancerSignAndSymptomsData_stampsTheVisitBeforeSaving() { + CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms(); + CancerSignAndSymptoms stored = new CancerSignAndSymptoms(); + stored.setID(10L); + when(cancerSignAndSymptomsRepo.save(symptoms)).thenReturn(stored); + + assertEquals(10L, service.saveCancerSignAndSymptomsData(symptoms, 1L, 2L)); + assertEquals(1L, symptoms.getBenVisitID()); + assertEquals(2L, symptoms.getVisitCode()); + + when(cancerSignAndSymptomsRepo.save(symptoms)).thenReturn(null); + assertNull(service.saveCancerSignAndSymptomsData(symptoms)); + } + + @Test + void saveCancerOralExaminationData_flattensThePreMalignantLesionTypes() { + CancerOralExamination oral = new CancerOralExamination(); + oral.setPreMalignantLesionTypeList(Arrays.asList("Leukoplakia", "Erythroplakia")); + CancerOralExamination stored = new CancerOralExamination(); + stored.setID(11L); + when(cancerOralExaminationRepo.save(oral)).thenReturn(stored); + + assertEquals(11L, service.saveCancerOralExaminationData(oral)); + assertEquals("Leukoplakia,Erythroplakia,", oral.getPreMalignantLesionType()); + + when(cancerOralExaminationRepo.save(oral)).thenReturn(null); + assertNull(service.saveCancerOralExaminationData(oral)); + } + + @Test + void saveCancerBreastExaminationData_returnsTheStoredId() { + CancerBreastExamination breast = new CancerBreastExamination(); + CancerBreastExamination stored = new CancerBreastExamination(); + stored.setID(12L); + when(cancerBreastExaminationRepo.save(breast)).thenReturn(stored); + assertEquals(12L, service.saveCancerBreastExaminationData(breast)); + + when(cancerBreastExaminationRepo.save(breast)).thenReturn(null); + assertNull(service.saveCancerBreastExaminationData(breast)); + } + + @Test + void saveCancerAbdominalExaminationData_returnsTheStoredId() { + CancerAbdominalExamination abdominal = new CancerAbdominalExamination(); + CancerAbdominalExamination stored = new CancerAbdominalExamination(); + stored.setID(13L); + when(cancerAbdominalExaminationRepo.save(abdominal)).thenReturn(stored); + assertEquals(13L, service.saveCancerAbdominalExaminationData(abdominal)); + + when(cancerAbdominalExaminationRepo.save(abdominal)).thenReturn(null); + assertNull(service.saveCancerAbdominalExaminationData(abdominal)); + } + + @Test + void saveCancerGynecologicalExaminationData_flattensTheLesionTypes() { + CancerGynecologicalExamination gynecological = new CancerGynecologicalExamination(); + gynecological.setTypeOfLesionList(Arrays.asList("Polyp", "Ulcer")); + CancerGynecologicalExamination stored = new CancerGynecologicalExamination(); + stored.setID(14L); + when(cancerGynecologicalExaminationRepo.save(gynecological)).thenReturn(stored); + + assertEquals(14L, service.saveCancerGynecologicalExaminationData(gynecological)); + assertEquals("Polyp,Ulcer,", gynecological.getTypeOfLesion()); + + when(cancerGynecologicalExaminationRepo.save(gynecological)).thenReturn(null); + assertNull(service.saveCancerGynecologicalExaminationData(gynecological)); + } + + @Test + void saveDocExaminationImageAnnotation_expandsEveryMarkerIntoItsOwnRow() { + WrapperCancerExamImgAnotasn wrapper = new WrapperCancerExamImgAnotasn(); + wrapper.setBeneficiaryRegID(1L); + wrapper.setImageID(2); + ArrayList> markers = new ArrayList<>(); + Map marker = new HashMap<>(); + marker.put("xCord", 10d); + marker.put("yCord", 20d); + marker.put("point", 1d); + marker.put("description", "lesion"); + markers.add(marker); + wrapper.setMarkers(markers); + + when(cancerExaminationImageAnnotationRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + assertEquals(1L, service.saveDocExaminationImageAnnotation(Collections.singletonList(wrapper), 3L, 4L)); + assertEquals(3L, wrapper.getVisitID()); + } + + @Test + void saveDocExaminationImageAnnotation_returnsNullWhenNoMarkerWasDrawn() { + WrapperCancerExamImgAnotasn wrapper = new WrapperCancerExamImgAnotasn(); + when(cancerExaminationImageAnnotationRepo.saveAll(any())).thenReturn(new ArrayList<>()); + assertNull(service.saveDocExaminationImageAnnotation(Collections.singletonList(wrapper), 3L, 4L)); + } + + @Test + void getCancerExaminationImageAnnotationList_ignoresEmptyWrappers() { + assertTrue(service.getCancerExaminationImageAnnotationList(new ArrayList<>(), 1L).isEmpty()); + assertTrue(service + .getCancerExaminationImageAnnotationList(Collections.singletonList(null), 1L).isEmpty()); + } + } + + @Nested + @DisplayName("examination updates") + class ExaminationUpdates { + + @Test + void updateSignAndSymptomsExaminationDetails_updatesTheStoredRowWhenOneExists() { + CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms(); + when(cancerSignAndSymptomsRepo.getCancerSignAndSymptomsStatus(any(), any())).thenReturn("P"); + when(cancerSignAndSymptomsRepo.updateCancerSignAndSymptoms(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + eq("U"))).thenReturn(1); + + assertEquals(1, service.updateSignAndSymptomsExaminationDetails(symptoms)); + } + + @Test + void updateSignAndSymptomsExaminationDetails_insertsAFreshRowWhenNoneIsStoredYet() { + CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms(); + CancerSignAndSymptoms stored = new CancerSignAndSymptoms(); + stored.setID(15L); + when(cancerSignAndSymptomsRepo.getCancerSignAndSymptomsStatus(any(), any())).thenReturn(null); + when(cancerSignAndSymptomsRepo.save(symptoms)).thenReturn(stored); + + assertEquals(1, service.updateSignAndSymptomsExaminationDetails(symptoms)); + } + + @Test + void updateLymphNodeExaminationDetails_clearsEveryNodeWhenTheEnlargementFlagIsTurnedOff() { + WrapperCancerSymptoms wrapper = new WrapperCancerSymptoms(); + CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms(); + symptoms.setLymphNode_Enlarged(false); + symptoms.setBeneficiaryRegID(1L); + symptoms.setVisitCode(2L); + wrapper.setCancerSignAndSymptoms(symptoms); + wrapper.setCancerLymphNodeDetails(new ArrayList<>()); + + when(cancerLymphNodeExaminationRepo.getCancerLymphNodeDetailsStatus(1L, 2L)) + .thenReturn(statusRows(16L, "P")); + when(cancerLymphNodeExaminationRepo.deleteExistingLymphNodeDetails(16L, "U")).thenReturn(1); + + assertEquals(1, service.updateLymphNodeExaminationDetails(wrapper)); + } + + @Test + void updateLymphNodeExaminationDetails_replacesOnlyTheReportedNodesWhenTheFlagIsOn() { + WrapperCancerSymptoms wrapper = new WrapperCancerSymptoms(); + CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms(); + symptoms.setLymphNode_Enlarged(true); + symptoms.setBeneficiaryRegID(1L); + symptoms.setVisitCode(2L); + wrapper.setCancerSignAndSymptoms(symptoms); + + CancerLymphNodeDetails measured = new CancerLymphNodeDetails(); + measured.setLymphNodeName("Cervical"); + measured.setSize_Left("2cm"); + CancerLymphNodeDetails unmeasured = new CancerLymphNodeDetails(); + unmeasured.setLymphNodeName("Cervical"); + wrapper.setCancerLymphNodeDetails(Arrays.asList(measured, unmeasured)); + + when(cancerLymphNodeExaminationRepo.getCancerLymphNodeDetailsStatusForLymphnodeNameList(eq(1L), eq(2L), + any())).thenReturn(statusRows(17L, "N")); + when(cancerLymphNodeExaminationRepo.deleteExistingLymphNodeDetails(17L, "N")).thenReturn(1); + when(cancerLymphNodeExaminationRepo.saveAll(any())) + .thenReturn(new ArrayList<>(Collections.singletonList(measured))); + + assertEquals(1, service.updateLymphNodeExaminationDetails(wrapper)); + } + + @Test + void updateLymphNodeExaminationDetails_succeedsWhenNoNodeWasMeasured() { + WrapperCancerSymptoms wrapper = new WrapperCancerSymptoms(); + CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms(); + symptoms.setLymphNode_Enlarged(true); + wrapper.setCancerSignAndSymptoms(symptoms); + wrapper.setCancerLymphNodeDetails(new ArrayList<>()); + + assertEquals(1, service.updateLymphNodeExaminationDetails(wrapper)); + } + + @Test + void updateLymphNodeExaminationDetails_reportsFailureWhenTheOldNodesCouldNotBeCleared() { + WrapperCancerSymptoms wrapper = new WrapperCancerSymptoms(); + CancerSignAndSymptoms symptoms = new CancerSignAndSymptoms(); + symptoms.setLymphNode_Enlarged(false); + wrapper.setCancerSignAndSymptoms(symptoms); + wrapper.setCancerLymphNodeDetails(new ArrayList<>()); + + when(cancerLymphNodeExaminationRepo.getCancerLymphNodeDetailsStatus(any(), any())) + .thenReturn(statusRows(18L, "N")); + when(cancerLymphNodeExaminationRepo.deleteExistingLymphNodeDetails(18L, "N")).thenReturn(0); + + assertEquals(0, service.updateLymphNodeExaminationDetails(wrapper)); + } + + @Test + void updateCancerOralDetails_updatesTheStoredRowWhenOneExists() { + CancerOralExamination oral = new CancerOralExamination(); + when(cancerOralExaminationRepo.getCancerOralExaminationStatus(any(), any())).thenReturn("P"); + when(cancerOralExaminationRepo.updateCancerOralExaminationDetails(any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), eq("U"))).thenReturn(1); + + assertEquals(1, service.updateCancerOralDetails(oral)); + } + + @Test + void updateCancerOralDetails_insertsAFreshRowWhenNoneIsStoredYet() { + CancerOralExamination oral = new CancerOralExamination(); + CancerOralExamination stored = new CancerOralExamination(); + stored.setID(19L); + when(cancerOralExaminationRepo.getCancerOralExaminationStatus(any(), any())).thenReturn(null); + when(cancerOralExaminationRepo.save(oral)).thenReturn(stored); + + assertEquals(1, service.updateCancerOralDetails(oral)); + } + + @Test + void updateCancerBreastDetails_updatesTheStoredRowWhenOneExists() { + CancerBreastExamination breast = new CancerBreastExamination(); + when(cancerBreastExaminationRepo.getCancerBreastExaminationStatus(any(), any())).thenReturn("P"); + when(cancerBreastExaminationRepo.updateCancerBreastExaminatio(any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"))) + .thenReturn(1); + + assertEquals(1, service.updateCancerBreastDetails(breast)); + } + + @Test + void updateCancerBreastDetails_insertsAFreshRowWhenNoneIsStoredYet() { + CancerBreastExamination breast = new CancerBreastExamination(); + CancerBreastExamination stored = new CancerBreastExamination(); + stored.setID(20L); + when(cancerBreastExaminationRepo.getCancerBreastExaminationStatus(any(), any())).thenReturn(null); + when(cancerBreastExaminationRepo.save(breast)).thenReturn(stored); + + assertEquals(1, service.updateCancerBreastDetails(breast)); + } + + @Test + void updateCancerAbdominalExaminationDetails_updatesTheStoredRowWhenOneExists() { + CancerAbdominalExamination abdominal = new CancerAbdominalExamination(); + when(cancerAbdominalExaminationRepo.getCancerAbdominalExaminationStatus(any(), any())).thenReturn("P"); + when(cancerAbdominalExaminationRepo.updateCancerAbdominalExamination(any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"))) + .thenReturn(1); + + assertEquals(1, service.updateCancerAbdominalExaminationDetails(abdominal)); + } + + @Test + void updateCancerAbdominalExaminationDetails_insertsAFreshRowWhenNoneIsStoredYet() { + CancerAbdominalExamination abdominal = new CancerAbdominalExamination(); + CancerAbdominalExamination stored = new CancerAbdominalExamination(); + stored.setID(21L); + when(cancerAbdominalExaminationRepo.getCancerAbdominalExaminationStatus(any(), any())).thenReturn(null); + when(cancerAbdominalExaminationRepo.save(abdominal)).thenReturn(stored); + + assertEquals(1, service.updateCancerAbdominalExaminationDetails(abdominal)); + } + + @Test + void updateCancerGynecologicalExaminationDetails_updatesTheStoredRowWhenOneExists() { + CancerGynecologicalExamination gynecological = new CancerGynecologicalExamination(); + when(cancerGynecologicalExaminationRepo.getCancerGynecologicalExaminationStatus(any(), any())) + .thenReturn("P"); + when(cancerGynecologicalExaminationRepo.updateCancerGynecologicalExamination(any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq("U"))).thenReturn(1); + + assertEquals(1, service.updateCancerGynecologicalExaminationDetails(gynecological)); + } + + @Test + void updateCancerGynecologicalExaminationDetails_insertsAFreshRowWhenNoneIsStoredYet() { + CancerGynecologicalExamination gynecological = new CancerGynecologicalExamination(); + CancerGynecologicalExamination stored = new CancerGynecologicalExamination(); + stored.setID(22L); + when(cancerGynecologicalExaminationRepo.getCancerGynecologicalExaminationStatus(any(), any())) + .thenReturn(null); + when(cancerGynecologicalExaminationRepo.save(gynecological)).thenReturn(stored); + + assertEquals(1, service.updateCancerGynecologicalExaminationDetails(gynecological)); + } + + @Test + void updateCancerExamImgAnotasnDetails_replacesTheAnnotationsOfEveryTouchedImage() { + CancerExaminationImageAnnotation complete = new CancerExaminationImageAnnotation(); + complete.setCancerImageID(1); + complete.setBeneficiaryRegID(1L); + complete.setVisitCode(2L); + complete.setxCoordinate(10); + complete.setyCoordinate(20); + complete.setCreatedBy("nurse"); + CancerExaminationImageAnnotation incomplete = new CancerExaminationImageAnnotation(); + incomplete.setCancerImageID(1); + List annotations = Arrays.asList(complete, incomplete); + + when(cancerExaminationImageAnnotationRepo.getCancerExaminationImageAnnotationDetailsStatus(eq(1L), eq(2L), + any())).thenReturn(statusRows(23L, "P")); + when(cancerExaminationImageAnnotationRepo.deleteExistingImageAnnotationDetails(23L, "U")).thenReturn(1); + when(cancerExaminationImageAnnotationRepo.saveAll(any())) + .thenReturn(new ArrayList<>(Collections.singletonList(complete))); + + assertEquals(1, service.updateCancerExamImgAnotasnDetails(annotations)); + assertEquals("nurse", complete.getModifiedBy()); + } + + @Test + void updateCancerExamImgAnotasnDetails_succeedsWhenNoImageWasAnnotated() { + assertEquals(1, service.updateCancerExamImgAnotasnDetails(new ArrayList<>())); + } + + @Test + void updateCancerExamImgAnotasnDetails_reportsFailureWhenTheOldAnnotationsCouldNotBeCleared() { + CancerExaminationImageAnnotation annotation = new CancerExaminationImageAnnotation(); + annotation.setCancerImageID(1); + List annotations = Collections.singletonList(annotation); + + when(cancerExaminationImageAnnotationRepo.getCancerExaminationImageAnnotationDetailsStatus(any(), any(), + any())).thenReturn(statusRows(24L, "N")); + when(cancerExaminationImageAnnotationRepo.deleteExistingImageAnnotationDetails(24L, "N")).thenReturn(0); + + assertEquals(0, service.updateCancerExamImgAnotasnDetails(annotations)); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/cancerScreening/CSServiceImplTest.java b/src/test/java/com/iemr/mmu/service/cancerScreening/CSServiceImplTest.java new file mode 100644 index 00000000..e3dac130 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/cancerScreening/CSServiceImplTest.java @@ -0,0 +1,636 @@ +/* +* 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.mmu.service.cancerScreening; + +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.anyLong; +import static org.mockito.ArgumentMatchers.anyShort; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.json.JSONObject; +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus; +import com.iemr.mmu.data.doctor.CancerDiagnosis; +import com.iemr.mmu.data.nurse.BenCancerVitalDetail; +import com.iemr.mmu.data.nurse.BeneficiaryVisitDetail; +import com.iemr.mmu.data.nurse.CommonUtilityClass; +import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo; +import com.iemr.mmu.repo.registrar.RegistrarRepoBenData; +import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl; +import com.iemr.mmu.service.tele_consultation.TeleConsultationServiceImpl; + +class CSServiceImplTest { + + @Mock + private CSNurseServiceImpl cSNurseServiceImpl; + @Mock + private CSDoctorServiceImpl cSDoctorServiceImpl; + @Mock + private CSOncologistServiceImpl csOncologistServiceImpl; + @Mock + private CommonNurseServiceImpl commonNurseServiceImpl; + @Mock + private CSCarestreamServiceImpl cSCarestreamServiceImpl; + @Mock + private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl; + @Mock + private TeleConsultationServiceImpl teleConsultationServiceImpl; + @Mock + private CommonDoctorServiceImpl commonDoctorServiceImpl; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + @Mock + private RegistrarRepoBenData registrarRepoBenData; + + @InjectMocks + private CSServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + private static String visitDetailsBlock() { + return "\"visitDetails\":{\"beneficiaryRegID\":1,\"visitReason\":\"Screening\"," + + "\"visitCategory\":\"Cancer Screening\"}"; + } + + @Nested + @DisplayName("saving nurse data") + class NurseSave { + + @Test + void saveCancerScreeningNurseData_ignoresARequestWithoutVisitDetails() throws Exception { + assertNull(service.saveCancerScreeningNurseData(null, "auth")); + assertNull(service.saveCancerScreeningNurseData(json("{}"), "auth")); + } + + @Test + void saveCancerScreeningNurseData_skipsTheVisitWhenTheNurseAlreadySavedThisFlow() throws Exception { + when(beneficiaryFlowStatusRepo.checkExistData(any(), any())).thenReturn(new BeneficiaryFlowStatus()); + + assertEquals(0L, service.saveCancerScreeningNurseData(json("{" + visitDetailsBlock() + "}"), "auth")); + verify(commonNurseServiceImpl, never()).saveBeneficiaryVisitDetails(any()); + } + + @Test + void saveCancerScreeningNurseData_returnsZeroWhenTheVisitWasNotCreated() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1); + + assertEquals(0L, service.saveCancerScreeningNurseData(json("{" + visitDetailsBlock() + "}"), "auth")); + } + + @Test + void saveCancerScreeningNurseData_sendsTheBeneficiaryToTheOncologistByDefault() throws Exception { + stubVisitCreation(); + when(cSNurseServiceImpl.saveBenVitalDetail(any())).thenReturn(1L); + + String request = "{" + visitDetailsBlock() + ",\"vitalsDetails\":{}}"; + + assertEquals(1L, service.saveCancerScreeningNurseData(json(request), "auth")); + verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), org.mockito.ArgumentMatchers.eq((short) 9), + org.mockito.ArgumentMatchers.eq((short) 0), org.mockito.ArgumentMatchers.eq((short) 0), + org.mockito.ArgumentMatchers.eq((short) 0), org.mockito.ArgumentMatchers.eq((short) 1), anyLong(), + any()); + } + + @Test + void saveCancerScreeningNurseData_sendsTheBeneficiaryToTheDoctorAndRadiologistWhenBothAreNeeded() + throws Exception { + stubVisitCreation(); + when(cSNurseServiceImpl.saveBenVitalDetail(any())).thenReturn(1L); + when(cSNurseServiceImpl.saveCancerBreastExaminationData(any())).thenReturn(1L); + when(beneficiaryFlowStatusRepo.getBenDataForCareStream(any())).thenReturn(new ArrayList<>()); + when(cSCarestreamServiceImpl.createMamographyRequest(any(), anyLong(), anyLong(), anyString())) + .thenReturn(1); + + String request = "{" + visitDetailsBlock() + ",\"vitalsDetails\":{},\"sendToDoctorWorklist\":true," + + "\"examinationDetails\":{\"breastDetails\":{\"beneficiaryRegID\":1,\"referredToMammogram\":true}}}"; + + assertEquals(2L, service.saveCancerScreeningNurseData(json(request), "auth")); + verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), org.mockito.ArgumentMatchers.eq((short) 9), + org.mockito.ArgumentMatchers.eq((short) 1), org.mockito.ArgumentMatchers.eq((short) 0), + org.mockito.ArgumentMatchers.eq((short) 1), org.mockito.ArgumentMatchers.eq((short) 0), anyLong(), + any()); + } + + @Test + void saveCancerScreeningNurseData_leavesTheFlowUntouchedWhenASectionFailsToSave() throws Exception { + stubVisitCreation(); + when(cSNurseServiceImpl.saveBenVitalDetail(any())).thenReturn(null); + + String request = "{" + visitDetailsBlock() + ",\"vitalsDetails\":{}}"; + + assertNull(service.saveCancerScreeningNurseData(json(request), "auth")); + verify(commonBenStatusFlowServiceImpl, never()).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), + anyLong(), anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any()); + } + + private void stubVisitCreation() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + } + + @Test + void saveBenVisitDetails_returnsNothingWhenAVisitWasAlreadyCreatedRecently() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1); + assertTrue(service.saveBenVisitDetails(new BeneficiaryVisitDetail(), new CommonUtilityClass()).isEmpty()); + } + } + + @Nested + @DisplayName("saving the individual nurse sections") + class NurseSections { + + @Test + void saveBenHistoryDetails_treatsAnAbsentHistoryBlockAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenHistoryDetails(json("{}"), 1L, 2L)); + assertEquals(1L, service.saveBenHistoryDetails(null, 1L, 2L)); + } + + @Test + void saveBenHistoryDetails_savesEverySectionThatWasSent() throws Exception { + when(cSNurseServiceImpl.saveBenFamilyCancerHistory(any())).thenReturn(1); + when(cSNurseServiceImpl.saveBenPersonalCancerHistory(any())).thenReturn(1L); + when(cSNurseServiceImpl.saveBenPersonalCancerDietHistory(any())).thenReturn(1L); + when(cSNurseServiceImpl.saveBenObstetricCancerHistory(any())).thenReturn(1L); + + String request = "{\"historyDetails\":{\"familyHistory\":{\"diseases\":[{\"diseaseType\":\"Breast\"}]}," + + "\"personalHistory\":{},\"pastObstetricHistory\":{}}}"; + + assertEquals(1L, service.saveBenHistoryDetails(json(request), 1L, 2L)); + } + + @Test + void saveBenHistoryDetails_treatsEachAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenHistoryDetails(json("{\"historyDetails\":{}}"), 1L, 2L)); + } + + @Test + void saveBenHistoryDetails_failsWhenTheFamilyHistoryCarriesNoDisease() { + // An empty disease array leaves the family-history flag unset, which the + // success check then dereferences. + String request = "{\"historyDetails\":{\"familyHistory\":{\"diseases\":[]}}}"; + assertThrows(NullPointerException.class, () -> service.saveBenHistoryDetails(json(request), 1L, 2L)); + } + + @Test + void saveBenHistoryDetails_reportsFailureWhenASectionCouldNotBeSaved() throws Exception { + when(cSNurseServiceImpl.saveBenPersonalCancerHistory(any())).thenReturn(0L); + + String request = "{\"historyDetails\":{\"personalHistory\":{}}}"; + assertNull(service.saveBenHistoryDetails(json(request), 1L, 2L)); + } + + @Test + void saveBenVitalsDetails_savesTheVitalsWhenTheyWereSent() throws Exception { + when(cSNurseServiceImpl.saveBenVitalDetail(any())).thenReturn(3L); + + assertEquals(3L, service.saveBenVitalsDetails(json("{\"vitalsDetails\":{}}"), 1L, 2L)); + assertEquals(1L, service.saveBenVitalsDetails(json("{}"), 1L, 2L)); + assertEquals(1L, service.saveBenVitalsDetails(null, 1L, 2L)); + } + + @Test + void saveBenFamilyHistoryDetails_isNotWiredUpYet() { + assertNull(service.saveBenFamilyHistoryDetails()); + } + + @Test + void saveBenExaminationDetails_treatsAnAbsentExaminationBlockAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenExaminationDetails(json("{}"), 1L, "auth", 2L, 3L)); + assertEquals(1L, service.saveBenExaminationDetails(null, 1L, "auth", 2L, 3L)); + } + + @Test + void saveBenExaminationDetails_treatsEachAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenExaminationDetails(json("{\"examinationDetails\":{}}"), 1L, "auth", 2L, + 3L)); + } + + @Test + void saveBenExaminationDetails_savesEverySectionThatWasSent() throws Exception { + when(cSNurseServiceImpl.saveCancerSignAndSymptomsData(any(), anyLong(), anyLong())).thenReturn(1L); + when(cSNurseServiceImpl.saveLymphNodeDetails(any(), anyLong(), anyLong())).thenReturn(1L); + when(cSNurseServiceImpl.saveCancerOralExaminationData(any())).thenReturn(1L); + when(cSNurseServiceImpl.saveCancerBreastExaminationData(any())).thenReturn(1L); + when(cSNurseServiceImpl.saveCancerAbdominalExaminationData(any())).thenReturn(1L); + when(cSNurseServiceImpl.saveCancerGynecologicalExaminationData(any())).thenReturn(1L); + when(cSNurseServiceImpl.saveDocExaminationImageAnnotation(any(), anyLong(), anyLong())).thenReturn(1L); + + String request = "{\"examinationDetails\":{\"signsDetails\":{\"cancerSignAndSymptoms\":{}," + + "\"cancerLymphNodeDetails\":[{}]},\"oralDetails\":{},\"breastDetails\":{}," + + "\"abdominalDetails\":{},\"gynecologicalDetails\":{},\"imageCoordinates\":[{}]}}"; + + assertEquals(1L, service.saveBenExaminationDetails(json(request), 1L, "auth", 2L, 3L)); + } + + @Test + void saveBenExaminationDetails_raisesAMammogramOrderWhenTheBeneficiaryWasReferred() throws Exception { + when(cSNurseServiceImpl.saveCancerBreastExaminationData(any())).thenReturn(1L); + when(beneficiaryFlowStatusRepo.getBenDataForCareStream(3L)).thenReturn(new ArrayList<>()); + when(cSCarestreamServiceImpl.createMamographyRequest(any(), anyLong(), anyLong(), anyString())) + .thenReturn(1); + + String request = "{\"examinationDetails\":{\"breastDetails\":{\"beneficiaryRegID\":1," + + "\"referredToMammogram\":true}}}"; + + assertEquals(2L, service.saveBenExaminationDetails(json(request), 1L, "auth", 2L, 3L)); + } + + @Test + void saveBenExaminationDetails_reportsAFailedMammogramOrder() throws Exception { + when(cSNurseServiceImpl.saveCancerBreastExaminationData(any())).thenReturn(1L); + when(beneficiaryFlowStatusRepo.getBenDataForCareStream(3L)).thenReturn(new ArrayList<>()); + when(cSCarestreamServiceImpl.createMamographyRequest(any(), anyLong(), anyLong(), anyString())) + .thenReturn(0); + + String request = "{\"examinationDetails\":{\"breastDetails\":{\"beneficiaryRegID\":1," + + "\"referredToMammogram\":true}}}"; + + assertEquals(3L, service.saveBenExaminationDetails(json(request), 1L, "auth", 2L, 3L)); + } + + @Test + void saveBenExaminationDetails_reportsFailureWhenASectionCouldNotBeSaved() throws Exception { + when(cSNurseServiceImpl.saveCancerOralExaminationData(any())).thenReturn(0L); + + String request = "{\"examinationDetails\":{\"oralDetails\":{}}}"; + assertNull(service.saveBenExaminationDetails(json(request), 1L, "auth", 2L, 3L)); + } + } + + @Nested + @DisplayName("updating nurse data from the doctor screen") + class NurseUpdates { + + @Test + void UpdateCSHistoryNurseData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1, service.UpdateCSHistoryNurseData(json("{}"))); + assertEquals(1, service.UpdateCSHistoryNurseData(null)); + } + + @Test + void UpdateCSHistoryNurseData_updatesEverySectionThatWasSent() throws Exception { + when(cSNurseServiceImpl.updateBeneficiaryFamilyCancerHistory(any())).thenReturn(1); + when(cSNurseServiceImpl.updateBenObstetricCancerHistory(any())).thenReturn(1); + when(cSNurseServiceImpl.updateBenPersonalCancerHistory(any())).thenReturn(1); + when(cSNurseServiceImpl.updateBenPersonalCancerDietHistory(any())).thenReturn(1); + + String request = "{\"familyHistory\":[{}],\"pastObstetricHistory\":{},\"personalHistory\":{}}"; + + assertEquals(1, service.UpdateCSHistoryNurseData(json(request))); + } + + @Test + void UpdateCSHistoryNurseData_treatsAnEmptyFamilyHistoryAsAlreadyDone() throws Exception { + assertEquals(1, service.UpdateCSHistoryNurseData(json("{\"familyHistory\":[]}"))); + } + + @Test + void UpdateCSHistoryNurseData_reportsFailureWhenASectionCouldNotBeUpdated() throws Exception { + when(cSNurseServiceImpl.updateBenObstetricCancerHistory(any())).thenReturn(0); + assertEquals(0, service.UpdateCSHistoryNurseData(json("{\"pastObstetricHistory\":{}}"))); + } + + @Test + void updateBenExaminationDetail_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1, service.updateBenExaminationDetail(json("{}"))); + assertEquals(1, service.updateBenExaminationDetail(null)); + } + + @Test + void updateBenExaminationDetail_updatesEverySectionThatWasSent() throws Exception { + when(cSNurseServiceImpl.updateSignAndSymptomsExaminationDetails(any())).thenReturn(1); + when(cSNurseServiceImpl.updateLymphNodeExaminationDetails(any())).thenReturn(1); + when(cSNurseServiceImpl.updateCancerOralDetails(any())).thenReturn(1); + when(cSNurseServiceImpl.updateCancerBreastDetails(any())).thenReturn(1); + when(cSNurseServiceImpl.updateCancerAbdominalExaminationDetails(any())).thenReturn(1); + when(cSNurseServiceImpl.updateCancerGynecologicalExaminationDetails(any())).thenReturn(1); + when(cSNurseServiceImpl.getCancerExaminationImageAnnotationList(any(), any())) + .thenReturn(new ArrayList<>()); + when(cSNurseServiceImpl.updateCancerExamImgAnotasnDetails(any())).thenReturn(1); + + String request = "{\"visitCode\":3,\"signsDetails\":{\"cancerSignAndSymptoms\":{}," + + "\"cancerLymphNodeDetails\":[{}]},\"oralDetails\":{},\"breastDetails\":{}," + + "\"abdominalDetails\":{},\"gynecologicalDetails\":{\"fileIDs\":[\"a\",\"b\"]}," + + "\"imageCoordinates\":[{}]}"; + + assertEquals(1, service.updateBenExaminationDetail(json(request))); + } + + @Test + void updateBenExaminationDetail_treatsAnEmptySignsBlockAsAlreadyDone() throws Exception { + assertEquals(1, service.updateBenExaminationDetail(json("{\"signsDetails\":{}}"))); + } + + @Test + void updateBenExaminationDetail_reportsFailureWhenASectionCouldNotBeUpdated() throws Exception { + when(cSNurseServiceImpl.updateCancerOralDetails(any())).thenReturn(0); + assertEquals(0, service.updateBenExaminationDetail(json("{\"oralDetails\":{}}"))); + } + + @Test + void updateBenVitalDetail_delegatesToTheNurseService() { + BenCancerVitalDetail vital = new BenCancerVitalDetail(); + when(cSNurseServiceImpl.updateBenVitalDetail(vital)).thenReturn(1); + assertEquals(1, service.updateBenVitalDetail(vital)); + } + } + + @Nested + @DisplayName("reading the nurse and doctor screens") + class Reads { + + /** One left-panel row, wide enough for the beneficiary summary mapper. */ + private ArrayList oneBenDetailRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[40]); + return rows; + } + + @Test + void getBenDataFrmNurseToDocVisitDetailsScreen_returnsTheStoredVisit() throws Exception { + BeneficiaryVisitDetail visit = new BeneficiaryVisitDetail(); + visit.setBenVisitID(1L); + when(commonNurseServiceImpl.getCSVisitDetails(1L, 2L)).thenReturn(visit); + + assertTrue(service.getBenDataFrmNurseToDocVisitDetailsScreen(1L, 2L).contains("benVisitDetails")); + + when(commonNurseServiceImpl.getCSVisitDetails(3L, 4L)).thenReturn(null); + assertEquals("{}", service.getBenDataFrmNurseToDocVisitDetailsScreen(3L, 4L)); + } + + @Test + void getBenDataFrmNurseToDocHistoryScreen_gathersEveryHistorySection() { + assertTrue(service.getBenDataFrmNurseToDocHistoryScreen(1L, 2L).contains("benFamilyHistory")); + verify(cSNurseServiceImpl).getBenPersonalCancerDietHistoryData(1L, 2L); + } + + @Test + void getBenDataFrmNurseToDocVitalScreen_gathersTheVitalsAndTheirTrend() { + when(commonNurseServiceImpl.getGraphicalTrendData(1L, "cancer screening")).thenReturn(new HashMap<>()); + assertTrue(service.getBenDataFrmNurseToDocVitalScreen(1L, 2L).contains("GraphData")); + } + + @Test + void getBenDataFrmNurseToDocExaminationScreen_gathersEveryExaminedSystem() { + when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(1L, 2L)) + .thenReturn(new ArrayList<>()); + + assertTrue(service.getBenDataFrmNurseToDocExaminationScreen(1L, 2L).contains("imageCoordinates")); + verify(cSNurseServiceImpl).getBenCancerOralExaminationData(1L, 2L); + } + + @Test + void getBenNurseDataForCaseSheet_gathersEverySectionOfTheNurseCaseSheet() { + when(cSNurseServiceImpl.getBeneficiaryVisitDetails(1L, 2L)).thenReturn(new BeneficiaryVisitDetail()); + + assertTrue(service.getBenNurseDataForCaseSheet(1L, 2L).contains("benVisitDetail")); + verify(cSNurseServiceImpl).getBenCancerLymphNodeDetailsData(1L, 2L); + } + + @Test + void getBenDataForCaseSheet_combinesTheNurseAndDoctorSections() throws Exception { + when(cSNurseServiceImpl.getBenNurseDataForCaseSheet(1L, 2L)).thenReturn(new HashMap<>()); + when(cSDoctorServiceImpl.getBenDoctorEnteredDataForCaseSheet(1L, 2L)).thenReturn(new HashMap<>()); + when(beneficiaryFlowStatusRepo.getBenDetailsForLeftSidePanel(1L, 3L)).thenReturn(oneBenDetailRow()); + when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(1L, 2L)) + .thenReturn(new ArrayList<>()); + + assertTrue(service.getBenDataForCaseSheet(3L, 1L, 2L, "auth").contains("ImageAnnotatedData")); + } + + @Test + void getCancerCasesheetData_readsTheBeneficiaryKeysOutOfTheRequest() throws Exception { + when(cSNurseServiceImpl.getBenNurseDataForCaseSheet(1L, 2L)).thenReturn(new HashMap<>()); + when(cSDoctorServiceImpl.getBenDoctorEnteredDataForCaseSheet(1L, 2L)).thenReturn(new HashMap<>()); + when(beneficiaryFlowStatusRepo.getBenDetailsForLeftSidePanel(1L, 3L)).thenReturn(oneBenDetailRow()); + when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(1L, 2L)) + .thenReturn(new ArrayList<>()); + + JSONObject request = new JSONObject(); + request.put("benRegID", 1L); + request.put("benVisitID", 4L); + request.put("benFlowID", 3L); + request.put("visitCode", 2L); + + assertTrue(service.getCancerCasesheetData(request, "auth").contains("BeneficiaryData")); + } + + @Test + void getCancerCasesheetData_returnsNothingForAnEmptyRequest() throws Exception { + assertNull(service.getCancerCasesheetData(new JSONObject(), "auth")); + } + + @Test + void getCancerCasesheetData_tolratesARequestMissingTheBeneficiaryKeys() throws Exception { + when(cSNurseServiceImpl.getBenNurseDataForCaseSheet(null, null)).thenReturn(new HashMap<>()); + when(cSDoctorServiceImpl.getBenDoctorEnteredDataForCaseSheet(null, null)).thenReturn(new HashMap<>()); + when(beneficiaryFlowStatusRepo.getBenDetailsForLeftSidePanel(null, null)).thenReturn(oneBenDetailRow()); + when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(null, null)) + .thenReturn(new ArrayList<>()); + + JSONObject request = new JSONObject(); + request.put("unrelated", 1); + request.put("alsoUnrelated", 2); + + assertTrue(service.getCancerCasesheetData(request, "auth").contains("BeneficiaryData")); + } + + @Test + void thePastHistoryReadsDelegateToTheNurseService() { + when(cSNurseServiceImpl.getBenCancerFamilyHistory(1L)).thenReturn("family"); + assertEquals("family", service.getBenFamilyHistoryData(1L)); + + when(cSNurseServiceImpl.getBenCancerPersonalHistory(1L)).thenReturn("personal"); + assertEquals("personal", service.getBenPersonalHistoryData(1L)); + + when(cSNurseServiceImpl.getBenCancerPersonalDietHistory(1L)).thenReturn("diet"); + assertEquals("diet", service.getBenPersonalDietHistoryData(1L)); + + when(cSNurseServiceImpl.getBenCancerObstetricHistory(1L)).thenReturn("obstetric"); + assertEquals("obstetric", service.getBenObstetricHistoryData(1L)); + } + + @Test + void theDoctorDiagnosisReadsDelegateToTheDoctorService() { + when(cSDoctorServiceImpl.getBenCancerDiagnosisData(1L, 2L)).thenReturn(new CancerDiagnosis()); + + assertTrue(service.getBenDoctorDiagnosisData(1L, 2L).contains("benDiagnosisDetails")); + assertTrue(service.getBenCaseRecordFromDoctorCS(1L, 2L).contains("diagnosis")); + } + } + + @Nested + @DisplayName("saving and updating doctor data") + class DoctorData { + + private String doctorRequest(String extra) { + return "{\"diagnosis\":{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"benFlowID\":4," + + "\"createdBy\":\"doctor\"" + extra + "}}"; + } + + @Test + void saveCancerScreeningDoctorData_savesTheDiagnosisAndAdvancesTheFlow() throws Exception { + when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(1L); + + assertEquals(1L, service.saveCancerScreeningDoctorData(json(doctorRequest("")), "auth")); + verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(), + anyShort(), anyShort(), anyShort(), anyInt(), any(), any()); + } + + @Test + void saveCancerScreeningDoctorData_ignoresARequestWithoutADiagnosis() throws Exception { + assertNull(service.saveCancerScreeningDoctorData(json("{}"), "auth")); + } + + @Test + void saveCancerScreeningDoctorData_leavesTheFlowUntouchedWhenTheDiagnosisWasNotStored() throws Exception { + when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(0L); + + assertNull(service.saveCancerScreeningDoctorData(json(doctorRequest("")), "auth")); + verify(commonBenStatusFlowServiceImpl, never()).updateBenFlowAfterDocData(any(), any(), any(), any(), + anyShort(), anyShort(), anyShort(), anyShort(), anyInt(), any(), any()); + } + + @Test + void saveCancerScreeningDoctorData_booksTheSpecialistSlotBeforeRaisingATeleconsultationRequest() + throws Exception { + when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(1L); + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(1); + when(teleConsultationServiceImpl.createTCRequest(any())).thenReturn(1); + + String request = "{\"diagnosis\":{\"beneficiaryRegID\":1,\"serviceID\":4,\"createdBy\":\"doctor\"}," + + "\"tcRequest\":{\"userID\":5,\"allocationDate\":\"2024-01-01\",\"fromTime\":\"10:00:00\"," + + "\"toTime\":\"10:30:00\"}}"; + + assertEquals(1L, service.saveCancerScreeningDoctorData(json(request), "auth")); + verify(teleConsultationServiceImpl).createTCRequest(any()); + } + + @Test + void saveCancerScreeningDoctorData_failsWhenTheSpecialistSlotCouldNotBeBooked() throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(0); + + String request = "{\"diagnosis\":{\"serviceID\":4,\"createdBy\":\"doctor\"}," + + "\"tcRequest\":{\"userID\":5,\"allocationDate\":\"2024-01-01\",\"fromTime\":\"10:00:00\"," + + "\"toTime\":\"10:30:00\"}}"; + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.saveCancerScreeningDoctorData(json(request), "auth")); + assertEquals("Error while booking slot.", thrown.getMessage()); + } + + @Test + void saveBenDiagnosisDetails_storesTheDiagnosisWhenOneWasSent() throws Exception { + when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(4L); + + assertEquals(4L, service.saveBenDiagnosisDetails(json(doctorRequest("")))); + assertEquals(1L, service.saveBenDiagnosisDetails(json("{}"))); + assertEquals(1L, service.saveBenDiagnosisDetails(null)); + } + + @Test + void saveBenDiagnosisDetails_reportsFailureWhenTheDiagnosisWasNotStored() throws Exception { + when(cSDoctorServiceImpl.saveCancerDiagnosisData(any())).thenReturn(0L); + assertNull(service.saveBenDiagnosisDetails(json(doctorRequest("")))); + } + + @Test + void updateCancerScreeningDoctorData_updatesTheDiagnosisAndAdvancesTheFlow() throws Exception { + when(cSDoctorServiceImpl.updateCancerDiagnosisDetailsByDoctor(any())).thenReturn(1); + when(beneficiaryFlowStatusRepo.updateBenFlowAfterTCSpcialistDoneForCanceScreening(4L, 1L, 3L)) + .thenReturn(1); + + assertEquals(1, service.updateCancerScreeningDoctorData(json(doctorRequest("")))); + } + + @Test + void updateCancerScreeningDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + when(cSDoctorServiceImpl.updateCancerDiagnosisDetailsByDoctor(any())).thenReturn(1); + when(beneficiaryFlowStatusRepo.updateBenFlowAfterTCSpcialistDoneForCanceScreening(any(), any(), any())) + .thenReturn(0); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.updateCancerScreeningDoctorData(json(doctorRequest("")))); + assertTrue(thrown.getMessage().contains("beneficiary flow status")); + } + + @Test + void updateCancerScreeningDoctorData_failsWhenTheDiagnosisCouldNotBeUpdated() throws Exception { + when(cSDoctorServiceImpl.updateCancerDiagnosisDetailsByDoctor(any())).thenReturn(0); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.updateCancerScreeningDoctorData(json(doctorRequest("")))); + assertEquals("Error while saving data.", thrown.getMessage()); + } + + @Test + void updateCancerScreeningDoctorData_rejectsARequestWithoutADiagnosis() { + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.updateCancerScreeningDoctorData(json("{}"))); + assertEquals("Invalid request.", thrown.getMessage()); + + RuntimeException nullRequest = assertThrows(RuntimeException.class, + () -> service.updateCancerScreeningDoctorData(null)); + assertEquals("Invalid request as it is null.", nullRequest.getMessage()); + } + + @Test + void updateCancerDiagnosisDetailsByOncologist_delegatesToTheOncologistService() { + CancerDiagnosis diagnosis = new CancerDiagnosis(); + when(csOncologistServiceImpl.updateCancerDiagnosisDetailsByOncologist(diagnosis)).thenReturn(1); + assertEquals(1, service.updateCancerDiagnosisDetailsByOncologist(diagnosis)); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/cancerScreening/CancerDoctorAndCarestreamServiceTest.java b/src/test/java/com/iemr/mmu/service/cancerScreening/CancerDoctorAndCarestreamServiceTest.java new file mode 100644 index 00000000..e22e4c1a --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/cancerScreening/CancerDoctorAndCarestreamServiceTest.java @@ -0,0 +1,235 @@ +/* +* 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.mmu.service.cancerScreening; + +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.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.when; + +import java.sql.Date; +import java.util.ArrayList; +import java.util.Arrays; +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.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +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.RestTemplate; + +import com.iemr.mmu.data.doctor.CancerDiagnosis; +import com.iemr.mmu.repo.doctor.CancerDiagnosisRepo; +import com.iemr.mmu.utils.CookieUtil; + +class CancerDoctorAndCarestreamServiceTest { + + @Nested + @DisplayName("CSDoctorServiceImpl") + class DoctorService { + + @Mock + private CancerDiagnosisRepo cancerDiagnosisRepo; + + @InjectMocks + private CSDoctorServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private CancerDiagnosis diagnosisWithServices(String... services) { + CancerDiagnosis diagnosis = new CancerDiagnosis(); + diagnosis.setBeneficiaryRegID(1L); + diagnosis.setVisitCode(2L); + diagnosis.setRefrredToAdditionalServiceList(Arrays.asList(services)); + return diagnosis; + } + + @Test + void saveCancerDiagnosisData_flattensTheAdditionalServicesBeforeSaving() { + CancerDiagnosis diagnosis = diagnosisWithServices("Radiology", "Oncology"); + CancerDiagnosis stored = new CancerDiagnosis(); + stored.setID(5L); + when(cancerDiagnosisRepo.save(diagnosis)).thenReturn(stored); + + assertEquals(5L, service.saveCancerDiagnosisData(diagnosis)); + assertEquals("Radiology,Oncology", diagnosis.getRefrredToAdditionalService()); + assertEquals(5L, stored.getVanSerialNo()); + } + + @Test + void saveCancerDiagnosisData_storesAnEmptyServiceListAsAnEmptyString() { + CancerDiagnosis diagnosis = new CancerDiagnosis(); + when(cancerDiagnosisRepo.save(diagnosis)).thenReturn(null); + + assertNull(service.saveCancerDiagnosisData(diagnosis)); + assertEquals("", diagnosis.getRefrredToAdditionalService()); + } + + @Test + void getBenCancerDiagnosisData_splitsTheStoredServicesBackIntoAList() { + CancerDiagnosis stored = new CancerDiagnosis(); + stored.setRefrredToAdditionalService("Radiology,Oncology"); + when(cancerDiagnosisRepo.getBenCancerDiagnosisDetails(1L, 2L)).thenReturn(stored); + + assertEquals(List.of("Radiology", "Oncology"), + service.getBenCancerDiagnosisData(1L, 2L).getRefrredToAdditionalServiceList()); + } + + @Test + void getBenCancerDiagnosisData_namesTheReferredInstituteWhenOneIsLinked() { + CancerDiagnosis stored = new CancerDiagnosis(); + com.iemr.mmu.data.institution.Institute institute = new com.iemr.mmu.data.institution.Institute(); + institute.setInstitutionName("District Hospital"); + stored.setInstitute(institute); + when(cancerDiagnosisRepo.getBenCancerDiagnosisDetails(1L, 2L)).thenReturn(stored); + + assertEquals("District Hospital", service.getBenCancerDiagnosisData(1L, 2L).getReferredToInstituteName()); + } + + @Test + void getBenCancerDiagnosisData_returnsNothingForABeneficiaryWithNoDiagnosis() { + when(cancerDiagnosisRepo.getBenCancerDiagnosisDetails(1L, 2L)).thenReturn(null); + + assertNull(service.getBenCancerDiagnosisData(1L, 2L)); + } + + @Test + void getBenDoctorEnteredDataForCaseSheet_carriesTheDiagnosis() { + CancerDiagnosis stored = new CancerDiagnosis(); + when(cancerDiagnosisRepo.getBenCancerDiagnosisDetails(1L, 2L)).thenReturn(stored); + + assertEquals(stored, service.getBenDoctorEnteredDataForCaseSheet(1L, 2L).get("diagnosis")); + } + + @Test + void updateCancerDiagnosisDetailsByDoctor_updatesTheStoredRowWhenOneExists() { + CancerDiagnosis diagnosis = diagnosisWithServices("Radiology"); + when(cancerDiagnosisRepo.getCancerDiagnosisStatuses(1L, 2L)).thenReturn("P"); + when(cancerDiagnosisRepo.updateCancerDiagnosisDetailsByDoctor(any(), any(), any(), anyString(), any(), + any(), any(), eq("U"), anyLong(), anyLong())).thenReturn(1); + + assertEquals(1, service.updateCancerDiagnosisDetailsByDoctor(diagnosis)); + } + + @Test + void updateCancerDiagnosisDetailsByDoctor_insertsAFreshRowWhenNoneIsStoredYet() { + CancerDiagnosis diagnosis = diagnosisWithServices("Radiology"); + CancerDiagnosis stored = new CancerDiagnosis(); + stored.setID(5L); + when(cancerDiagnosisRepo.getCancerDiagnosisStatuses(1L, 2L)).thenReturn(null); + when(cancerDiagnosisRepo.save(diagnosis)).thenReturn(stored); + + assertEquals(1, service.updateCancerDiagnosisDetailsByDoctor(diagnosis)); + + stored.setID(0L); + assertEquals(0, service.updateCancerDiagnosisDetailsByDoctor(diagnosis)); + } + } + + @Nested + @DisplayName("CSCarestreamServiceImpl") + class CarestreamService { + + @Mock + private CookieUtil cookieUtil; + + @InjectMocks + private CSCarestreamServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(service, "carestreamOrderCreateURL", "http://carestream/order"); + } + + /** One beneficiary row as the flow-status query returns it. */ + private ArrayList beneficiary(String name, short genderID) { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { name, "Devi", Date.valueOf("1990-05-04"), genderID }); + return rows; + } + + private MockedConstruction carestreamAnswering(String body) { + return mockConstruction(RestTemplate.class, + (mock, context) -> when( + mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(body, HttpStatus.OK))); + } + + @Test + void createMamographyRequest_reportsAnAcceptedOrder() { + try (MockedConstruction rest = carestreamAnswering("{\"statusCode\":200}")) { + assertEquals(1, service.createMamographyRequest(beneficiary("Asha Devi", (short) 2), 1L, 2L, "auth")); + } + } + + @Test + void createMamographyRequest_reportsARejectedOrder() { + try (MockedConstruction rest = carestreamAnswering("{\"statusCode\":500}")) { + assertEquals(0, service.createMamographyRequest(beneficiary("Asha Devi", (short) 2), 1L, 2L, "auth")); + } + } + + @Test + void createMamographyRequest_mapsEveryGenderCodeCarestreamExpects() { + try (MockedConstruction rest = carestreamAnswering("{\"statusCode\":200}")) { + assertEquals(1, service.createMamographyRequest(beneficiary("Ram", (short) 1), 1L, 2L, "auth")); + assertEquals(1, service.createMamographyRequest(beneficiary("Asha Devi", (short) 2), 1L, 2L, "auth")); + assertEquals(1, service.createMamographyRequest(beneficiary("Kiran", (short) 3), 1L, 2L, "auth")); + } + } + + @Test + void createMamographyRequest_sendsAnEmptyOrderForABeneficiaryWithNoDetails() { + try (MockedConstruction rest = carestreamAnswering("{\"statusCode\":200}")) { + assertEquals(1, service.createMamographyRequest(new ArrayList<>(), 1L, 2L, "auth")); + } + } + + @Test + void createMamographyRequest_reportsAnOrderThatCouldNotBeSent() { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when( + mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenThrow(new RuntimeException("carestream unreachable")))) { + + assertEquals(0, service.createMamographyRequest(beneficiary("Asha Devi", (short) 2), 1L, 2L, "auth")); + } + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/common/transaction/CommonDoctorServiceImplTest.java b/src/test/java/com/iemr/mmu/service/common/transaction/CommonDoctorServiceImplTest.java new file mode 100644 index 00000000..cf17108c --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/common/transaction/CommonDoctorServiceImplTest.java @@ -0,0 +1,627 @@ +/* +* 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.mmu.service.common.transaction; + +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; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyShort; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collections; +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.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.util.ReflectionTestUtils; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.anc.WrapperAncFindings; +import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus; +import com.iemr.mmu.data.login.Users; +import com.iemr.mmu.data.nurse.CommonUtilityClass; +import com.iemr.mmu.data.quickConsultation.BenChiefComplaint; +import com.iemr.mmu.data.quickConsultation.BenClinicalObservations; +import com.iemr.mmu.data.snomedct.SCTDescription; +import com.iemr.mmu.data.tele_consultation.TeleconsultationRequestOBJ; +import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo; +import com.iemr.mmu.repo.doctor.BenReferDetailsRepo; +import com.iemr.mmu.repo.doctor.DocWorkListRepo; +import com.iemr.mmu.repo.login.UserLoginRepo; +import com.iemr.mmu.repo.nurse.BenVisitDetailRepo; +import com.iemr.mmu.repo.quickConsultation.BenChiefComplaintRepo; +import com.iemr.mmu.repo.quickConsultation.BenClinicalObservationsRepo; +import com.iemr.mmu.repo.quickConsultation.LabTestOrderDetailRepo; +import com.iemr.mmu.repo.quickConsultation.PrescribedDrugDetailRepo; +import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo; +import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl; +import com.iemr.mmu.service.snomedct.SnomedServiceImpl; +import com.iemr.mmu.utils.CookieUtil; + +class CommonDoctorServiceImplTest { + + @Mock + private BenClinicalObservationsRepo benClinicalObservationsRepo; + @Mock + private BenChiefComplaintRepo benChiefComplaintRepo; + @Mock + private DocWorkListRepo docWorkListRepo; + @Mock + private BenReferDetailsRepo benReferDetailsRepo; + @Mock + private LabTestOrderDetailRepo labTestOrderDetailRepo; + @Mock + private PrescribedDrugDetailRepo prescribedDrugDetailRepo; + @Mock + private PrescriptionDetailRepo prescriptionDetailRepo; + @Mock + private SnomedServiceImpl snomedServiceImpl; + @Mock + private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + @Mock + private CookieUtil cookieUtil; + @Mock + private BenVisitDetailRepo benVisitDetailRepo; + @Mock + private UserLoginRepo userLoginRepo; + + @InjectMocks + private CommonDoctorServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(service, "tmReferCheckValue", "Tele-consultation"); + } + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + private static WrapperAncFindings findings(BenChiefComplaint... complaints) { + ArrayList list = new ArrayList<>(); + Collections.addAll(list, complaints); + return new WrapperAncFindings(1L, 2L, 4, "Alert", "Fever", "Anaemia", list, Boolean.FALSE, 3L); + } + + private static BenChiefComplaint complaint(String name) { + BenChiefComplaint complaint = new BenChiefComplaint(); + complaint.setChiefComplaint(name); + complaint.setBenChiefComplaintID(9L); + return complaint; + } + + @Nested + @DisplayName("findings") + class Findings { + + @Test + void saveFindings_reportsWhetherTheObservationWasStored() throws Exception { + BenClinicalObservations stored = new BenClinicalObservations(); + when(benClinicalObservationsRepo.save(any())).thenReturn(stored); + assertEquals(1, service.saveFindings(json("{}"))); + + when(benClinicalObservationsRepo.save(any())).thenReturn(null); + assertEquals(0, service.saveFindings(json("{}"))); + } + + @Test + void saveDocFindings_storesTheObservationAndEveryNamedComplaint() { + when(benClinicalObservationsRepo.save(any())).thenReturn(new BenClinicalObservations()); + when(benChiefComplaintRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + WrapperAncFindings wrapper = findings(complaint("Fever"), new BenChiefComplaint()); + + assertEquals(1, service.saveDocFindings(wrapper)); + verify(benChiefComplaintRepo).updateVanSerialNo(9L); + } + + @Test + void saveDocFindings_succeedsWhenNoComplaintWasNamed() { + when(benClinicalObservationsRepo.save(any())).thenReturn(new BenClinicalObservations()); + assertEquals(1, service.saveDocFindings(findings())); + verify(benChiefComplaintRepo, never()).saveAll(any()); + } + + @Test + void saveDocFindings_reportsFailureWhenTheObservationWasNotStored() { + when(benClinicalObservationsRepo.save(any())).thenReturn(null); + assertEquals(0, service.saveDocFindings(findings())); + } + + @Test + void saveDocFindings_reportsFailureWhenNotEveryComplaintWasStored() { + when(benClinicalObservationsRepo.save(any())).thenReturn(new BenClinicalObservations()); + when(benChiefComplaintRepo.saveAll(any())).thenReturn(new ArrayList<>()); + + assertEquals(0, service.saveDocFindings(findings(complaint("Fever")))); + } + + @Test + void getSnomedCTcode_looksUpEverySymptomAndFallsBackWhenOneIsUnknown() { + SCTDescription known = new SCTDescription(); + known.setConceptID("111"); + known.setTerm("Fever"); + when(snomedServiceImpl.findSnomedCTRecordFromTerm("Fever")).thenReturn(known); + when(snomedServiceImpl.findSnomedCTRecordFromTerm("Unknown")).thenReturn(null); + + assertArrayEquals(new String[] { "111,N/A", "Fever,N/A" }, + service.getSnomedCTcode("Fever, Unknown")); + } + + @Test + void getSnomedCTcode_returnsEmptyCodesForNoSymptoms() { + assertArrayEquals(new String[] { "", "" }, service.getSnomedCTcode(null)); + assertArrayEquals(new String[] { "", "" }, service.getSnomedCTcode("")); + } + + @Test + void fetchBenPreviousSignificantFindings_mapsEveryStoredFinding() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { new java.sql.Date(System.currentTimeMillis()), "Anaemia" }); + when(benClinicalObservationsRepo.getPreviousSignificantFindings(1L)).thenReturn(rows); + + assertTrue(service.fetchBenPreviousSignificantFindings(1L).contains("Anaemia")); + } + + @Test + void getFindingsDetails_combinesTheObservationsAndComplaints() { + when(benClinicalObservationsRepo.getFindingsData(1L, 2L)).thenReturn(new ArrayList<>()); + when(benChiefComplaintRepo.getBenChiefComplaints(1L, 2L)).thenReturn(new ArrayList<>()); + + assertNotNull(service.getFindingsDetails(1L, 2L)); + } + + @Test + void updateDocFindings_updatesTheObservationAndEveryNamedComplaint() { + when(benClinicalObservationsRepo.getBenClinicalObservationStatus(1L, 3L)).thenReturn("P"); + when(benClinicalObservationsRepo.updateBenClinicalObservations(any(), any(), any(), any(), any(), any(), + any(), eq("U"), anyLong(), anyLong())).thenReturn(1); + when(benChiefComplaintRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + assertEquals(1, service.updateDocFindings(findings(complaint("Fever")))); + } + + @Test + void updateDocFindings_succeedsWhenNoComplaintWasNamed() { + when(benClinicalObservationsRepo.getBenClinicalObservationStatus(any(), any())).thenReturn("N"); + when(benClinicalObservationsRepo.updateBenClinicalObservations(any(), any(), any(), any(), any(), any(), + any(), eq("N"), anyLong(), anyLong())).thenReturn(1); + + assertEquals(1, service.updateDocFindings(findings())); + } + + @Test + void updateDocFindings_reportsFailureWhenTheObservationCouldNotBeUpdated() { + when(benClinicalObservationsRepo.getBenClinicalObservationStatus(any(), any())).thenReturn("N"); + when(benClinicalObservationsRepo.updateBenClinicalObservations(any(), any(), any(), any(), any(), any(), + any(), any(), anyLong(), anyLong())).thenReturn(0); + + assertEquals(0, service.updateDocFindings(findings())); + } + + @Test + void updateBenClinicalObservations_insertsAFreshRowWhenNoneIsStoredYet() { + BenClinicalObservations observations = new BenClinicalObservations(); + BenClinicalObservations stored = new BenClinicalObservations(); + stored.setClinicalObservationID(5L); + when(benClinicalObservationsRepo.getBenClinicalObservationStatus(any(), any())).thenReturn(null); + when(benClinicalObservationsRepo.save(observations)).thenReturn(stored); + + assertEquals(1, service.updateBenClinicalObservations(observations)); + } + + @Test + void updateBenClinicalObservations_reportsFailureWhenTheFreshRowWasNotStored() { + BenClinicalObservations observations = new BenClinicalObservations(); + BenClinicalObservations stored = new BenClinicalObservations(); + stored.setClinicalObservationID(0L); + when(benClinicalObservationsRepo.getBenClinicalObservationStatus(any(), any())).thenReturn(null); + when(benClinicalObservationsRepo.save(observations)).thenReturn(stored); + + assertEquals(0, service.updateBenClinicalObservations(observations)); + assertEquals(0, service.updateBenClinicalObservations(null)); + } + + @Test + void updateDoctorBenChiefComplaints_succeedsWhenThereIsNothingToUpdate() { + assertEquals(1, service.updateDoctorBenChiefComplaints(null)); + assertEquals(1, service.updateDoctorBenChiefComplaints(new ArrayList<>())); + } + + @Test + void updateDoctorBenChiefComplaints_reportsNothingSavedWhenTheStoreDropsTheRows() { + List complaints = Collections.singletonList(complaint("Fever")); + when(benChiefComplaintRepo.saveAll(complaints)).thenReturn(new ArrayList<>()); + + assertEquals(0, service.updateDoctorBenChiefComplaints(complaints)); + } + } + + @Nested + @DisplayName("work lists") + class WorkLists { + + @Test + void getDocWorkList_serialisesTheStoredWorklist() { + when(docWorkListRepo.getDocWorkList()).thenReturn(new ArrayList<>()); + assertNotNull(service.getDocWorkList()); + } + + @Test + void getDocWorkListNew_readsTheMmuWorklistWithinTheConfiguredWindow() { + ReflectionTestUtils.setField(service, "docWL", 10); + when(beneficiaryFlowStatusRepo.getDocWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>()); + + assertEquals("[]", service.getDocWorkListNew(1, 2, 3)); + } + + @Test + void getDocWorkListNew_readsTheTeleconsultationWorklist() { + when(beneficiaryFlowStatusRepo.getDocWorkListNewTC(1)).thenReturn(new ArrayList<>()); + assertEquals("[]", service.getDocWorkListNew(1, 4, 3)); + } + + @Test + void getDocWorkListNew_returnsNothingForAnUnknownService() { + assertEquals("[]", service.getDocWorkListNew(1, 9, 3)); + } + + @Test + void getDocWorkListNewFutureScheduledForTM_onlyAppliesToTeleconsultation() { + when(beneficiaryFlowStatusRepo.getDocWorkListNewFutureScheduledTC(1)).thenReturn(new ArrayList<>()); + + assertEquals("[]", service.getDocWorkListNewFutureScheduledForTM(1, 4)); + assertEquals("[]", service.getDocWorkListNewFutureScheduledForTM(1, 2)); + } + + @Test + void getTCSpecialistWorkListNewForTM_onlyAppliesToTeleconsultation() { + when(beneficiaryFlowStatusRepo.getTCSpecialistWorkListNew(1, 5)).thenReturn(new ArrayList<>()); + + assertEquals("[]", service.getTCSpecialistWorkListNewForTM(1, 5, 4)); + assertEquals("[]", service.getTCSpecialistWorkListNewForTM(1, 5, 2)); + } + + @Test + void getTCSpecialistWorkListNewFutureScheduledForTM_onlyAppliesToTeleconsultation() { + when(beneficiaryFlowStatusRepo.getTCSpecialistWorkListNewFutureScheduled(1, 5)) + .thenReturn(new ArrayList<>()); + + assertEquals("[]", service.getTCSpecialistWorkListNewFutureScheduledForTM(1, 5, 4)); + assertEquals("[]", service.getTCSpecialistWorkListNewFutureScheduledForTM(1, 5, 2)); + } + } + + @Nested + @DisplayName("referrals") + class Referrals { + + private String referRequest(String extra) { + return "{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"providerServiceMapID\":4," + + "\"createdBy\":\"doctor\"" + extra + "}"; + } + + @Test + void saveBenReferDetails_createsOneRowPerAdditionalService() throws Exception { + when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + String request = referRequest(",\"referredToInstituteID\":7,\"referredToInstituteName\":\"PHC\"," + + "\"referralReason\":\"Fever\",\"revisitDate\":\"2024-01-01T00:00:00.000\"," + + "\"refrredToAdditionalServiceList\":[{\"serviceID\":1,\"serviceName\":\"Tele-consultation\"}," + + "{\"serviceID\":2,\"serviceName\":\"Lab\"},{\"serviceID\":3}]"); + + assertEquals(1L, service.saveBenReferDetails(json(request))); + } + + @Test + void saveBenReferDetails_storesTheReferralAsOneRowWhenNoServiceWasChosen() throws Exception { + when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + assertEquals(1L, service.saveBenReferDetails(json(referRequest( + ",\"referredToInstituteName\":\"PHC\"")))); + } + + @Test + void saveBenReferDetails_storesNothingWhenTheReferralIsEmpty() throws Exception { + when(benReferDetailsRepo.saveAll(any())).thenReturn(new ArrayList<>()); + assertEquals(1L, service.saveBenReferDetails(json(referRequest("")))); + } + + @Test + void saveBenReferDetailsTMreferred_reusesTheExistingRowWhenTheInstituteWasUpdated() throws Exception { + when(benReferDetailsRepo.updateReferredInstituteNameTMReferred(any(), any(), any(), eq("U"))) + .thenReturn(1); + + assertEquals(1L, service.saveBenReferDetailsTMreferred(json(referRequest( + ",\"referredToInstituteID\":7,\"referredToInstituteName\":\"PHC\"")))); + } + + @Test + void saveBenReferDetailsTMreferred_createsOneRowPerAdditionalServiceWhenNoRowExisted() throws Exception { + when(benReferDetailsRepo.updateReferredInstituteNameTMReferred(any(), any(), any(), any())).thenReturn(0); + when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + String request = referRequest(",\"referredToInstituteID\":7,\"referredToInstituteName\":\"PHC\"," + + "\"revisitDate\":\"2024-01-01T00:00:00.000\"," + + "\"refrredToAdditionalServiceList\":[{\"serviceID\":1,\"serviceName\":\"Lab\"},{}]"); + + assertEquals(1L, service.saveBenReferDetailsTMreferred(json(request))); + } + + @Test + void saveBenReferDetailsTMreferred_storesTheReferralAsOneRowWhenNoServiceWasChosen() throws Exception { + when(benReferDetailsRepo.updateReferredInstituteNameTMReferred(any(), any(), any(), any())).thenReturn(0); + when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + assertEquals(1L, service.saveBenReferDetailsTMreferred( + json(referRequest(",\"referredToInstituteName\":\"PHC\"")))); + } + + @Test + void updateBenReferDetails_refreshesTheStoredRowsAndAddsTheNewServices() throws Exception { + ArrayList statuses = new ArrayList<>(); + statuses.add(new Object[] { 5L, "P", "Lab" }); + when(benReferDetailsRepo.getBenReferDetailsStatus(1L, 3L)).thenReturn(statuses); + when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + String request = referRequest(",\"referredToInstituteID\":7,\"referredToInstituteName\":\"PHC\"," + + "\"referralReason\":\"Fever\",\"revisitDate\":\"2024-01-01T00:00:00.000\"," + + "\"refrredToAdditionalServiceList\":[{\"serviceID\":1,\"serviceName\":\"Lab\"}," + + "{\"serviceID\":2,\"serviceName\":\"Tele-consultation\"},{\"serviceID\":3," + + "\"serviceName\":\"Radiology\"}]"); + + assertEquals(1L, service.updateBenReferDetails(json(request))); + verify(benReferDetailsRepo).updateReferredInstituteName(any(), any(), any(), any(), eq(5L), eq("U")); + } + + @Test + void updateBenReferDetails_keepsARowThatWasNeverSyncedMarkedAsNew() throws Exception { + ArrayList statuses = new ArrayList<>(); + statuses.add(new Object[] { 5L, "N", "Lab" }); + when(benReferDetailsRepo.getBenReferDetailsStatus(any(), any())).thenReturn(statuses); + when(benReferDetailsRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + assertEquals(1L, service.updateBenReferDetails( + json(referRequest(",\"referredToInstituteName\":\"PHC\"")))); + verify(benReferDetailsRepo).updateReferredInstituteName(any(), any(), any(), any(), eq(5L), eq("N")); + } + + @Test + void updateBenReferDetails_leavesTheStoredRowsAloneWhenNothingWasEntered() throws Exception { + when(benReferDetailsRepo.getBenReferDetailsStatus(any(), any())).thenReturn(new ArrayList<>()); + when(benReferDetailsRepo.saveAll(any())).thenReturn(new ArrayList<>()); + + assertEquals(1L, service.updateBenReferDetails(json(referRequest("")))); + } + + @Test + void getReferralDetails_mapsTheStoredReferral() { + when(benReferDetailsRepo.getBenReferDetails(1L, 2L)).thenReturn(new ArrayList<>()); + assertNotNull(service.getReferralDetails(1L, 2L)); + } + } + + @Nested + @DisplayName("investigations and prescriptions") + class InvestigationsAndPrescriptions { + + @Test + void getInvestigationDetails_mapsTheStoredOrders() { + when(labTestOrderDetailRepo.getLabTestOrderDetails(1L, 2L)).thenReturn(new ArrayList<>()); + assertNotNull(service.getInvestigationDetails(1L, 2L)); + } + + @Test + void getPrescribedDrugs_mapsTheStoredDrugs() { + when(prescribedDrugDetailRepo.getBenPrescribedDrugDetails(1L, 2L)).thenReturn(new ArrayList<>()); + assertEquals("[]", service.getPrescribedDrugs(1L, 2L)); + } + + @Test + void deletePrescribedMedicine_reportsWhetherTheRowWasRemoved() throws Exception { + JSONObject request = new JSONObject(); + request.put("id", 5L); + when(prescribedDrugDetailRepo.deletePrescribedmedicine(5L)).thenReturn(1); + assertEquals("record deleted successfully", service.deletePrescribedMedicine(request)); + + when(prescribedDrugDetailRepo.deletePrescribedmedicine(5L)).thenReturn(0); + assertNull(service.deletePrescribedMedicine(request)); + assertNull(service.deletePrescribedMedicine(new JSONObject())); + assertNull(service.deletePrescribedMedicine(null)); + } + } + + @Nested + @DisplayName("beneficiary flow after doctor data") + class BeneficiaryFlow { + + private CommonUtilityClass utility(Boolean isSpecialist) { + CommonUtilityClass utility = new CommonUtilityClass(); + utility.setBenFlowID(1L); + utility.setBeneficiaryID(2L); + utility.setBenVisitID(3L); + utility.setBeneficiaryRegID(4L); + utility.setVisitCode(5L); + utility.setCreatedBy("doctor"); + utility.setIsSpecialist(isSpecialist); + return utility; + } + + private TeleconsultationRequestOBJ teleconsultationRequest() { + TeleconsultationRequestOBJ request = new TeleconsultationRequestOBJ(); + request.setUserID(7); + request.setAllocationDate(new Timestamp(System.currentTimeMillis())); + return request; + } + + @Test + void updateBenFlowtableAfterDocDataSave_recordsTheDoctorAndSendsTheBeneficiaryToTheLabAndPharmacy() + throws Exception { + Users doctor = new Users(); + doctor.setUserID(11L); + when(userLoginRepo.getUserByUsername("doctor")).thenReturn(doctor); + when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(), + anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility(false), true, true, + teleconsultationRequest(), true)); + verify(benVisitDetailRepo).updateDoctorID(11L, 5L); + verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocData(eq(1L), eq(4L), eq(2L), eq(3L), + eq((short) 2), eq((short) 1), eq((short) 0), eq((short) 1), eq(7), any(), eq(true)); + } + + @Test + void updateBenFlowtableAfterDocDataSave_closesTheDoctorStepWhenNothingWasPrescribed() throws Exception { + when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(), + anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility(false), false, false, null, false)); + verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocData(eq(1L), eq(4L), eq(2L), eq(3L), + eq((short) 9), eq((short) 0), eq((short) 0), eq((short) 0), eq(0), any(), eq(false)); + } + + @Test + void updateBenFlowtableAfterDocDataSave_marksAnNcdScreeningTeleconsultationReferral() throws Exception { + ReflectionTestUtils.setField(service, "TMReferred", 1); + BeneficiaryFlowStatus stored = new BeneficiaryFlowStatus(); + stored.setVisitCategory("NCD screening"); + when(beneficiaryFlowStatusRepo.specialistFlagAndCategoryValue(5L)).thenReturn(stored); + when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(), + anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility(false), false, false, null, false)); + verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(), + anyShort(), anyShort(), eq((short) 100), anyInt(), any(), any()); + } + + @Test + void updateBenFlowtableAfterDocDataSave_leavesTheDoctorUnresolvedWhenTheUsernameIsUnknown() throws Exception { + CommonUtilityClass utility = utility(false); + utility.setCreatedBy(" "); + when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(), + anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility, false, false, null, false)); + verify(benVisitDetailRepo, never()).updateDoctorID(anyLong(), anyLong()); + } + + @Test + void updateBenFlowtableAfterDocDataSave_leavesTheDoctorUnresolvedWhenTheVisitHasNoCode() throws Exception { + CommonUtilityClass utility = utility(false); + utility.setVisitCode(null); + when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocData(any(), any(), any(), any(), anyShort(), + anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenFlowtableAfterDocDataSave(utility, false, false, null, false)); + verify(benVisitDetailRepo, never()).updateDoctorID(anyLong(), anyLong()); + } + + @Test + void updateBenFlowtableAfterDocDataUpdate_routesASpecialistUpdateThroughTheSpecialistFlow() throws Exception { + when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocDataUpdateTCSpecialist(any(), any(), any(), + any(), anyShort(), anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenFlowtableAfterDocDataUpdate(utility(true), true, true, null, true)); + verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocDataUpdateTCSpecialist(eq(1L), eq(4L), eq(2L), + eq(3L), eq((short) 0), eq((short) 1), eq((short) 0), eq((short) 2), eq(0), any(), eq(true)); + } + + @Test + void updateBenFlowtableAfterDocDataUpdate_closesTheSpecialistStepWhenNothingWasPrescribed() throws Exception { + when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocDataUpdateTCSpecialist(any(), any(), any(), + any(), anyShort(), anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenFlowtableAfterDocDataUpdate(utility(true), false, false, null, false)); + verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocDataUpdateTCSpecialist(any(), any(), any(), + any(), anyShort(), eq((short) 0), anyShort(), eq((short) 9), anyInt(), any(), any()); + } + + @Test + void updateBenFlowtableAfterDocDataUpdate_usesTheWalkInFlowForAnOrdinaryDoctorUpdate() throws Exception { + when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocDataUpdateWDF(any(), any(), any(), any(), + anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenFlowtableAfterDocDataUpdate(utility(false), true, true, + teleconsultationRequest(), true)); + verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocDataUpdateWDF(eq(1L), eq(4L), eq(2L), eq(3L), + eq((short) 2), eq((short) 1), eq((short) 0), eq(7), any(), eq(true)); + } + + @Test + void updateBenFlowtableAfterDocDataUpdate_usesTheReferralFlowForAnNcdScreeningReferral() throws Exception { + ReflectionTestUtils.setField(service, "TMReferred", 1); + BeneficiaryFlowStatus stored = new BeneficiaryFlowStatus(); + stored.setVisitCategory("NCD screening"); + when(beneficiaryFlowStatusRepo.specialistFlagAndCategoryValue(5L)).thenReturn(stored); + when(commonBenStatusFlowServiceImpl.updateBenFlowAfterDocDataUpdate(any(), any(), any(), any(), + anyShort(), anyShort(), anyShort(), anyShort(), anyInt(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenFlowtableAfterDocDataUpdate(utility(false), false, false, null, false)); + verify(commonBenStatusFlowServiceImpl).updateBenFlowAfterDocDataUpdate(any(), any(), any(), any(), + eq((short) 9), eq((short) 0), anyShort(), eq((short) 100), anyInt(), any(), any()); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/common/transaction/CommonNurseServiceImplTest.java b/src/test/java/com/iemr/mmu/service/common/transaction/CommonNurseServiceImplTest.java new file mode 100644 index 00000000..dfe792e1 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/common/transaction/CommonNurseServiceImplTest.java @@ -0,0 +1,2479 @@ +/* +* 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.mmu.service.common.transaction; + +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.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Date; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.iemr.mmu.data.anc.BenAllergyHistory; +import com.iemr.mmu.data.anc.BenFamilyHistory; +import com.iemr.mmu.data.anc.BenMedHistory; +import com.iemr.mmu.data.anc.BenMedicationHistory; +import com.iemr.mmu.data.anc.BenMenstrualDetails; +import com.iemr.mmu.data.anc.BenPersonalHabit; +import com.iemr.mmu.data.anc.BencomrbidityCondDetails; +import com.iemr.mmu.data.anc.ChildOptionalVaccineDetail; +import com.iemr.mmu.data.anc.ChildVaccineDetail1; +import com.iemr.mmu.data.anc.FemaleObstetricHistory; +import com.iemr.mmu.data.anc.PhyGeneralExamination; +import com.iemr.mmu.data.anc.PhyHeadToToeExamination; +import com.iemr.mmu.data.anc.SysCardiovascularExamination; +import com.iemr.mmu.data.anc.SysCentralNervousExamination; +import com.iemr.mmu.data.anc.SysGastrointestinalExamination; +import com.iemr.mmu.data.anc.SysGenitourinarySystemExamination; +import com.iemr.mmu.data.anc.SysMusculoskeletalSystemExamination; +import com.iemr.mmu.data.anc.SysRespiratoryExamination; +import com.iemr.mmu.data.anc.WrapperChildOptionalVaccineDetail; +import com.iemr.mmu.data.anc.WrapperComorbidCondDetails; +import com.iemr.mmu.data.anc.WrapperFemaleObstetricHistory; +import com.iemr.mmu.data.anc.WrapperImmunizationHistory; +import com.iemr.mmu.data.anc.WrapperMedicationHistory; +import com.iemr.mmu.data.login.Users; +import com.iemr.mmu.data.ncdScreening.IDRSData; +import com.iemr.mmu.data.ncdScreening.PhysicalActivityType; +import com.iemr.mmu.data.nurse.BenAnthropometryDetail; +import com.iemr.mmu.data.nurse.BenPhysicalVitalDetail; +import com.iemr.mmu.data.nurse.BeneficiaryVisitDetail; +import com.iemr.mmu.data.quickConsultation.BenChiefComplaint; +import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo; +import com.iemr.mmu.repo.bmiCalculation.BMICalculationRepo; +import com.iemr.mmu.repo.login.UserLoginRepo; +import com.iemr.mmu.repo.nurse.BenAnthropometryRepo; +import com.iemr.mmu.repo.nurse.BenCancerVitalDetailRepo; +import com.iemr.mmu.repo.nurse.BenPhysicalVitalRepo; +import com.iemr.mmu.repo.nurse.BenVisitDetailRepo; +import com.iemr.mmu.repo.nurse.anc.BenAdherenceRepo; +import com.iemr.mmu.repo.nurse.anc.BenAllergyHistoryRepo; +import com.iemr.mmu.repo.nurse.anc.BenChildDevelopmentHistoryRepo; +import com.iemr.mmu.repo.nurse.anc.BenFamilyHistoryRepo; +import com.iemr.mmu.repo.nurse.anc.BenMedHistoryRepo; +import com.iemr.mmu.repo.nurse.anc.BenMedicationHistoryRepo; +import com.iemr.mmu.repo.nurse.anc.BenMenstrualDetailsRepo; +import com.iemr.mmu.repo.nurse.anc.BenPersonalHabitRepo; +import com.iemr.mmu.repo.nurse.anc.BencomrbidityCondRepo; +import com.iemr.mmu.repo.nurse.anc.ChildFeedingDetailsRepo; +import com.iemr.mmu.repo.nurse.anc.ChildOptionalVaccineDetailRepo; +import com.iemr.mmu.repo.nurse.anc.ChildVaccineDetail1Repo; +import com.iemr.mmu.repo.nurse.anc.FemaleObstetricHistoryRepo; +import com.iemr.mmu.repo.nurse.anc.PerinatalHistoryRepo; +import com.iemr.mmu.repo.nurse.anc.PhyGeneralExaminationRepo; +import com.iemr.mmu.repo.nurse.anc.PhyHeadToToeExaminationRepo; +import com.iemr.mmu.repo.nurse.anc.SysCardiovascularExaminationRepo; +import com.iemr.mmu.repo.nurse.anc.SysCentralNervousExaminationRepo; +import com.iemr.mmu.repo.nurse.anc.SysGastrointestinalExaminationRepo; +import com.iemr.mmu.repo.nurse.anc.SysGenitourinarySystemExaminationRepo; +import com.iemr.mmu.repo.nurse.anc.SysMusculoskeletalSystemExaminationRepo; +import com.iemr.mmu.repo.nurse.anc.SysRespiratoryExaminationRepo; +import com.iemr.mmu.repo.nurse.ncdscreening.IDRSDataRepo; +import com.iemr.mmu.repo.nurse.ncdscreening.PhysicalActivityTypeRepo; +import com.iemr.mmu.repo.quickConsultation.BenChiefComplaintRepo; +import com.iemr.mmu.repo.quickConsultation.LabTestOrderDetailRepo; +import com.iemr.mmu.repo.quickConsultation.PrescribedDrugDetailRepo; +import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo; +import com.iemr.mmu.repo.registrar.RegistrarRepoBenData; +import com.iemr.mmu.repo.registrar.ReistrarRepoBenSearch; +import com.iemr.mmu.utils.AESEncryption.AESEncryptionDecryption; +import com.iemr.mmu.utils.exception.IEMRException; + +class CommonNurseServiceImplTest { + + @Mock + private BenVisitDetailRepo benVisitDetailRepo; + @Mock + private UserLoginRepo userLoginRepo; + @Mock + private BenChiefComplaintRepo benChiefComplaintRepo; + @Mock + private BenMedHistoryRepo benMedHistoryRepo; + @Mock + private BencomrbidityCondRepo bencomrbidityCondRepo; + @Mock + private BenMedicationHistoryRepo benMedicationHistoryRepo; + @Mock + private FemaleObstetricHistoryRepo femaleObstetricHistoryRepo; + @Mock + private BenMenstrualDetailsRepo benMenstrualDetailsRepo; + @Mock + private BenFamilyHistoryRepo benFamilyHistoryRepo; + @Mock + private BenPersonalHabitRepo benPersonalHabitRepo; + @Mock + private BenAllergyHistoryRepo benAllergyHistoryRepo; + @Mock + private ChildOptionalVaccineDetailRepo childOptionalVaccineDetailRepo; + @Mock + private ChildVaccineDetail1Repo childVaccineDetail1Repo; + @Mock + private BenAnthropometryRepo benAnthropometryRepo; + @Mock + private BenPhysicalVitalRepo benPhysicalVitalRepo; + @Mock + private PhyGeneralExaminationRepo phyGeneralExaminationRepo; + @Mock + private PhyHeadToToeExaminationRepo phyHeadToToeExaminationRepo; + @Mock + private SysGastrointestinalExaminationRepo sysGastrointestinalExaminationRepo; + @Mock + private SysCardiovascularExaminationRepo sysCardiovascularExaminationRepo; + @Mock + private SysRespiratoryExaminationRepo sysRespiratoryExaminationRepo; + @Mock + private SysCentralNervousExaminationRepo sysCentralNervousExaminationRepo; + @Mock + private SysMusculoskeletalSystemExaminationRepo sysMusculoskeletalSystemExaminationRepo; + @Mock + private SysGenitourinarySystemExaminationRepo sysGenitourinarySystemExaminationRepo; + @Mock + private RegistrarRepoBenData registrarRepoBenData; + @Mock + private PrescriptionDetailRepo prescriptionDetailRepo; + @Mock + private LabTestOrderDetailRepo labTestOrderDetailRepo; + @Mock + private PrescribedDrugDetailRepo prescribedDrugDetailRepo; + @Mock + private ReistrarRepoBenSearch reistrarRepoBenSearch; + @Mock + private BenAdherenceRepo benAdherenceRepo; + @Mock + private BenChildDevelopmentHistoryRepo benChildDevelopmentHistoryRepo; + @Mock + private ChildFeedingDetailsRepo childFeedingDetailsRepo; + @Mock + private PerinatalHistoryRepo perinatalHistoryRepo; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + @Mock + private BenCancerVitalDetailRepo benCancerVitalDetailRepo; + @Mock + private CommonDoctorServiceImpl commonDoctorServiceImpl; + @Mock + private PhysicalActivityTypeRepo physicalActivityTypeRepo; + @Mock + private AESEncryptionDecryption aESEncryptionDecryption; + @Mock + private IDRSDataRepo iDRSDataRepo; + @Mock + private BMICalculationRepo bmiCalculationRepo; + + @InjectMocks + private CommonNurseServiceImpl service; + + private AutoCloseable mocks; + + @BeforeEach + void setUp() { + mocks = MockitoAnnotations.openMocks(this); + } + + @Nested + @DisplayName("beneficiary visit details") + class VisitDetails { + + @Test + void saveBeneficiaryVisitDetails_incrementsVisitCountAndResolvesNurse() { + BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail(); + detail.setBeneficiaryRegID(11L); + detail.setCreatedBy("nurse1"); + detail.setFileIDs(new String[] { "a", "b" }); + + Users user = new Users(); + user.setUserID(77L); + when(userLoginRepo.getUserByUsername("nurse1")).thenReturn(user); + when(benVisitDetailRepo.getVisitCountForBeneficiary(11L)).thenReturn((short) 3); + + BeneficiaryVisitDetail saved = new BeneficiaryVisitDetail(); + saved.setBenVisitID(99L); + when(benVisitDetailRepo.save(any(BeneficiaryVisitDetail.class))).thenReturn(saved); + + assertEquals(99L, service.saveBeneficiaryVisitDetails(detail)); + assertEquals((short) 4, detail.getVisitNo()); + assertEquals("a,b,", detail.getReportFilePath()); + assertEquals(77L, detail.getNurseID()); + verify(benVisitDetailRepo).updateVanSerialNo(99L); + } + + @Test + void saveBeneficiaryVisitDetails_startsAtVisitOneWhenBeneficiaryHasNoHistory() { + BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail(); + detail.setBeneficiaryRegID(11L); + when(benVisitDetailRepo.getVisitCountForBeneficiary(11L)).thenReturn(null); + + BeneficiaryVisitDetail saved = new BeneficiaryVisitDetail(); + saved.setBenVisitID(5L); + when(benVisitDetailRepo.save(any(BeneficiaryVisitDetail.class))).thenReturn(saved); + + assertEquals(5L, service.saveBeneficiaryVisitDetails(detail)); + assertEquals((short) 1, detail.getVisitNo()); + assertEquals("", detail.getReportFilePath()); + assertNull(detail.getNurseID()); + } + + @Test + void saveBeneficiaryVisitDetails_leavesNurseUnresolvedWhenUsernameIsUnknown() { + BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail(); + detail.setCreatedBy(" "); + + BeneficiaryVisitDetail saved = new BeneficiaryVisitDetail(); + saved.setBenVisitID(1L); + when(benVisitDetailRepo.save(any(BeneficiaryVisitDetail.class))).thenReturn(saved); + + service.saveBeneficiaryVisitDetails(detail); + assertNull(detail.getNurseID()); + verify(userLoginRepo, never()).getUserByUsername(anyString()); + } + + @Test + void saveBeneficiaryVisitDetails_leavesNurseUnresolvedWhenLookupFindsNoUser() { + BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail(); + detail.setCreatedBy("ghost"); + when(userLoginRepo.getUserByUsername("ghost")).thenReturn(null); + + BeneficiaryVisitDetail saved = new BeneficiaryVisitDetail(); + saved.setBenVisitID(1L); + when(benVisitDetailRepo.save(any(BeneficiaryVisitDetail.class))).thenReturn(saved); + + service.saveBeneficiaryVisitDetails(detail); + assertNull(detail.getNurseID()); + } + + @Test + void getBenVisitCount_returnsNextVisitNumber() { + when(benVisitDetailRepo.getVisitCountForBeneficiary(1L)).thenReturn((short) 2); + assertEquals((short) 3, service.getBenVisitCount(1L)); + + when(benVisitDetailRepo.getVisitCountForBeneficiary(2L)).thenReturn(null); + assertEquals((short) 1, service.getBenVisitCount(2L)); + } + + @Test + void updateBeneficiaryStatus_delegatesToRegistrarRepo() { + when(registrarRepoBenData.updateBenFlowStatus('N', 4L)).thenReturn(1); + assertEquals(1, service.updateBeneficiaryStatus('N', 4L)); + } + + @Test + void getMaxCurrentdate_returnsZeroWhenNoPreviousVisitExists() throws Exception { + when(benVisitDetailRepo.getMaxCreatedDate(1L, "reason", "category")).thenReturn(null); + assertEquals(0, service.getMaxCurrentdate(1L, "reason", "category")); + } + + @Test + void getMaxCurrentdate_returnsPositiveWhileThePreviousVisitIsStillWithinTenMinutes() throws Exception { + String recent = new java.sql.Timestamp(System.currentTimeMillis()).toString(); + when(benVisitDetailRepo.getMaxCreatedDate(1L, "reason", "category")).thenReturn(recent); + assertTrue(service.getMaxCurrentdate(1L, "reason", "category") > 0); + } + + @Test + void getMaxCurrentdate_returnsNegativeOnceThePreviousVisitIsOlderThanTenMinutes() throws Exception { + String old = new java.sql.Timestamp(System.currentTimeMillis() - 3600_000L).toString(); + when(benVisitDetailRepo.getMaxCreatedDate(1L, "reason", "category")).thenReturn(old); + assertTrue(service.getMaxCurrentdate(1L, "reason", "category") < 0); + } + + @Test + void getMaxCurrentdate_wrapsAnUnparseableStoredDate() { + when(benVisitDetailRepo.getMaxCreatedDate(1L, "r", "c")).thenReturn("not-a-date.0"); + IEMRException thrown = assertThrows(IEMRException.class, + () -> service.getMaxCurrentdate(1L, "r", "c")); + assertTrue(thrown.getMessage().contains("Error while parseing created date")); + } + + @Test + void generateVisitCode_padsVanAndVisitIdsIntoAFourteenDigitCode() { + when(benVisitDetailRepo.updateVisitCode(anyLong(), anyLong())).thenReturn(1); + assertEquals(Long.valueOf("10000100000123"), service.generateVisitCode(123L, 1, 1)); + } + + @Test + void generateVisitCode_returnsZeroWhenTheCodeCouldNotBeStored() { + when(benVisitDetailRepo.updateVisitCode(anyLong(), anyLong())).thenReturn(0); + assertEquals(0L, service.generateVisitCode(123L, 1, 1)); + } + + @Test + void updateVisitCodeInVisitDetailsTable_delegatesToRepo() { + when(benVisitDetailRepo.updateVisitCode(5L, 6L)).thenReturn(1); + assertEquals(1, service.updateVisitCodeInVisitDetailsTable(5L, 6L)); + } + + @Test + void updateBeneficiaryVisitDetails_returnsRepoResult() { + BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail(); + when(benVisitDetailRepo.updateBeneficiaryVisitDetail(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any())).thenReturn(1); + assertEquals(1, service.updateBeneficiaryVisitDetails(detail)); + } + + @Test + void updateBeneficiaryVisitDetails_returnsZeroWhenTheRepoFails() { + BeneficiaryVisitDetail detail = new BeneficiaryVisitDetail(); + when(benVisitDetailRepo.updateBeneficiaryVisitDetail(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any())).thenThrow(new RuntimeException("db down")); + assertEquals(0, service.updateBeneficiaryVisitDetails(detail)); + } + + @Test + void getCSVisitDetails_returnsNullWhenTheVisitDoesNotExist() throws Exception { + when(benVisitDetailRepo.getVisitDetails(1L, 2L)).thenReturn(null); + assertNull(service.getCSVisitDetails(1L, 2L)); + } + + @Test + void getCSVisitDetails_decryptsEachAttachedReportPath() throws Exception { + BeneficiaryVisitDetail stored = new BeneficiaryVisitDetail(); + stored.setBenVisitID(1L); + stored.setReportFilePath("enc1,,enc2"); + when(benVisitDetailRepo.getVisitDetails(1L, 2L)).thenReturn(stored); + when(aESEncryptionDecryption.decrypt("enc1")).thenReturn("/tmp/reports/first.pdf"); + when(aESEncryptionDecryption.decrypt("enc2")).thenReturn("/tmp/reports/second.pdf"); + + BeneficiaryVisitDetail result = service.getCSVisitDetails(1L, 2L); + + assertNotNull(result); + assertEquals(2, result.getFiles().size()); + assertEquals("first.pdf", result.getFiles().get(0).get("fileName")); + assertEquals("enc2", result.getFiles().get(1).get("filePath")); + } + + @Test + void getCSVisitDetails_returnsAnEmptyFileListWhenNoReportsAreAttached() throws Exception { + BeneficiaryVisitDetail stored = new BeneficiaryVisitDetail(); + stored.setBenVisitID(1L); + stored.setReportFilePath(" "); + when(benVisitDetailRepo.getVisitDetails(1L, 2L)).thenReturn(stored); + + assertTrue(service.getCSVisitDetails(1L, 2L).getFiles().isEmpty()); + } + } + + @Nested + @DisplayName("history and examination saves") + class HistorySaves { + + @Test + void saveBenChiefComplaints_skipsEntriesWithoutAComplaintId() { + BenChiefComplaint withId = new BenChiefComplaint(); + withId.setChiefComplaintID(1); + withId.setBenChiefComplaintID(10L); + BenChiefComplaint withoutId = new BenChiefComplaint(); + + when(benChiefComplaintRepo.saveAll(any())).thenReturn(Collections.singletonList(withId)); + + assertEquals(1, service.saveBenChiefComplaints(Arrays.asList(withId, withoutId))); + verify(benChiefComplaintRepo).updateVanSerialNo(10L); + } + + @Test + void saveBenChiefComplaints_succeedsWhenThereIsNothingToSave() { + assertEquals(1, service.saveBenChiefComplaints(new ArrayList<>())); + verify(benChiefComplaintRepo, never()).saveAll(any()); + } + + @Test + void saveBenChiefComplaints_reportsFailureWhenNotEveryComplaintWasSaved() { + BenChiefComplaint withId = new BenChiefComplaint(); + withId.setChiefComplaintID(1); + when(benChiefComplaintRepo.saveAll(any())).thenReturn(new ArrayList<>()); + assertEquals(0, service.saveBenChiefComplaints(Collections.singletonList(withId))); + } + + @Test + void saveBenPastHistory_savesEveryPastHistoryEntry() { + BenMedHistory history = mock(BenMedHistory.class); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenMedHistory())); + when(history.getBenPastHistory()).thenReturn(entries); + when(benMedHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1L, service.saveBenPastHistory(history)); + } + + @Test + void saveBenPastHistory_succeedsWhenThereIsNoPastHistory() { + BenMedHistory history = mock(BenMedHistory.class); + when(history.getBenPastHistory()).thenReturn(new ArrayList<>()); + assertEquals(1L, service.saveBenPastHistory(history)); + } + + @Test + void saveBenPastHistory_reportsFailureWhenNotEveryEntryWasSaved() { + BenMedHistory history = mock(BenMedHistory.class); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenMedHistory())); + when(history.getBenPastHistory()).thenReturn(entries); + when(benMedHistoryRepo.saveAll(entries)).thenReturn(new ArrayList()); + assertNull(service.saveBenPastHistory(history)); + } + + @Test + void saveBenComorbidConditions_returnsTheIdOfTheFirstStoredCondition() { + WrapperComorbidCondDetails wrapper = mock(WrapperComorbidCondDetails.class); + BencomrbidityCondDetails stored = new BencomrbidityCondDetails(); + stored.setID(42L); + ArrayList entries = new ArrayList<>(Collections.singletonList(stored)); + when(wrapper.getComrbidityConds()).thenReturn(entries); + when(bencomrbidityCondRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(42L, service.saveBenComorbidConditions(wrapper)); + } + + @Test + void saveBenComorbidConditions_succeedsWhenThereAreNoConditions() { + WrapperComorbidCondDetails wrapper = mock(WrapperComorbidCondDetails.class); + when(wrapper.getComrbidityConds()).thenReturn(new ArrayList<>()); + assertEquals(1L, service.saveBenComorbidConditions(wrapper)); + } + + @Test + void saveBenMedicationHistory_returnsTheIdOfTheFirstStoredEntry() { + WrapperMedicationHistory wrapper = mock(WrapperMedicationHistory.class); + BenMedicationHistory stored = new BenMedicationHistory(); + stored.setID(7L); + ArrayList entries = new ArrayList<>(Collections.singletonList(stored)); + when(wrapper.getBenMedicationHistoryDetails()).thenReturn(entries); + when(benMedicationHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(7L, service.saveBenMedicationHistory(wrapper)); + } + + @Test + void saveBenMedicationHistory_succeedsWhenThereIsNoMedicationHistory() { + WrapperMedicationHistory wrapper = mock(WrapperMedicationHistory.class); + when(wrapper.getBenMedicationHistoryDetails()).thenReturn(new ArrayList<>()); + assertEquals(1L, service.saveBenMedicationHistory(wrapper)); + } + + @Test + void saveFemaleObstetricHistory_flattensEveryComplicationListOntoTheStoredRow() { + FemaleObstetricHistory entry = new FemaleObstetricHistory(); + entry.setPregComplicationList(complications("pregComplicationID", "pregComplicationType")); + entry.setDeliveryComplicationList(complications("deliveryComplicationID", "deliveryComplicationType")); + entry.setPostpartumComplicationList( + complications("postpartumComplicationID", "postpartumComplicationType")); + + ArrayList> postAbortion = new ArrayList<>(); + postAbortion.add(complication(1d, "first")); + postAbortion.add(complication(2d, "second")); + entry.setPostAbortionComplication(postAbortion); + entry.setAbortionType(complication(3d, "abortion")); + + Map facility = new HashMap<>(); + facility.put("serviceFacilityID", 4d); + facility.put("facilityName", "PHC"); + entry.setTypeofFacility(facility); + + WrapperFemaleObstetricHistory wrapper = mock(WrapperFemaleObstetricHistory.class); + ArrayList entries = new ArrayList<>(Collections.singletonList(entry)); + when(wrapper.getFemaleObstetricHistoryDetails()).thenReturn(entries); + when(femaleObstetricHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1L, service.saveFemaleObstetricHistory(wrapper)); + assertEquals("11,12", entry.getPregComplicationID()); + assertEquals("name11,name12", entry.getPregComplicationType()); + assertEquals("11,12", entry.getDeliveryComplicationID()); + assertEquals("11,12", entry.getPostpartumComplicationID()); + assertEquals("1,2", entry.getPostAbortionComplication_db()); + assertEquals("first,second", entry.getPostAbortionComplicationsValues()); + assertEquals(3, entry.getAbortionTypeID()); + assertEquals("abortion", entry.getTypeOfAbortionValue()); + assertEquals(4, entry.getTypeofFacilityID()); + assertEquals("PHC", entry.getServiceFacilityValue()); + } + + @Test + void saveFemaleObstetricHistory_leavesComplicationFieldsEmptyWhenNoneWereReported() { + FemaleObstetricHistory entry = new FemaleObstetricHistory(); + WrapperFemaleObstetricHistory wrapper = mock(WrapperFemaleObstetricHistory.class); + ArrayList entries = new ArrayList<>(Collections.singletonList(entry)); + when(wrapper.getFemaleObstetricHistoryDetails()).thenReturn(entries); + when(femaleObstetricHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1L, service.saveFemaleObstetricHistory(wrapper)); + assertEquals("", entry.getPregComplicationID()); + assertNull(entry.getPostAbortionComplication_db()); + } + + @Test + void saveFemaleObstetricHistory_succeedsWhenThereIsNoObstetricHistory() { + WrapperFemaleObstetricHistory wrapper = mock(WrapperFemaleObstetricHistory.class); + when(wrapper.getFemaleObstetricHistoryDetails()).thenReturn(new ArrayList<>()); + assertEquals(1L, service.saveFemaleObstetricHistory(wrapper)); + } + + private ArrayList> complications(String idKey, String nameKey) { + ArrayList> list = new ArrayList<>(); + for (int i = 11; i <= 12; i++) { + Map item = new HashMap<>(); + item.put(idKey, i); + item.put(nameKey, "name" + i); + list.add(item); + } + return list; + } + + private Map complication(Double id, String value) { + Map map = new HashMap<>(); + map.put("complicationID", id); + map.put("complicationValue", value); + return map; + } + + @Test + void saveBenMenstrualHistory_flattensTheReportedProblemsBeforeSaving() { + BenMenstrualDetails details = new BenMenstrualDetails(); + ArrayList> problems = new ArrayList<>(); + for (int i = 1; i <= 2; i++) { + Map problem = new HashMap<>(); + problem.put("menstrualProblemID", i); + problem.put("problemName", "problem" + i); + problems.add(problem); + } + details.setMenstrualProblemList(problems); + + BenMenstrualDetails stored = new BenMenstrualDetails(); + stored.setBenMenstrualID(9); + when(benMenstrualDetailsRepo.save(details)).thenReturn(stored); + + assertEquals(9, service.saveBenMenstrualHistory(details)); + assertEquals("1,2", details.getMenstrualProblemID()); + assertEquals("problem1,problem2", details.getProblemName()); + } + + @Test + void saveBenMenstrualHistory_returnsNullWhenTheRowWasNotPersisted() { + BenMenstrualDetails details = new BenMenstrualDetails(); + BenMenstrualDetails stored = new BenMenstrualDetails(); + stored.setBenMenstrualID(0); + when(benMenstrualDetailsRepo.save(details)).thenReturn(stored); + assertNull(service.saveBenMenstrualHistory(details)); + } + + @Test + void saveBenFamilyHistory_savesEveryReportedFamilyDisease() { + BenFamilyHistory input = mock(BenFamilyHistory.class); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenFamilyHistory())); + when(input.getBenFamilyHistory()).thenReturn(entries); + when(benFamilyHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1L, service.saveBenFamilyHistory(input)); + } + + @Test + void saveBenFamilyHistory_succeedsWhenThereIsNoFamilyHistory() { + BenFamilyHistory input = mock(BenFamilyHistory.class); + when(input.getBenFamilyHistory()).thenReturn(new ArrayList<>()); + assertEquals(1L, service.saveBenFamilyHistory(input)); + } + + @Test + void saveBenFamilyHistoryNCDScreening_savesTheScreeningVariantOfTheList() { + BenFamilyHistory input = mock(BenFamilyHistory.class); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenFamilyHistory())); + when(input.getBenFamilyHist()).thenReturn(entries); + when(benFamilyHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1L, service.saveBenFamilyHistoryNCDScreening(input)); + } + + @Test + void saveBenFamilyHistoryNCDScreening_succeedsWhenNothingWasScreened() { + BenFamilyHistory input = mock(BenFamilyHistory.class); + when(input.getBenFamilyHist()).thenReturn(new ArrayList<>()); + assertEquals(1L, service.saveBenFamilyHistoryNCDScreening(input)); + } + + @Test + void savePersonalHistory_savesEveryReportedHabit() { + BenPersonalHabit input = mock(BenPersonalHabit.class); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenPersonalHabit())); + when(input.getPersonalHistory()).thenReturn(entries); + when(benPersonalHabitRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1, service.savePersonalHistory(input)); + } + + @Test + void savePersonalHistory_succeedsWhenNoHabitsWereReported() { + BenPersonalHabit input = mock(BenPersonalHabit.class); + when(input.getPersonalHistory()).thenReturn(new ArrayList<>()); + assertEquals(1, service.savePersonalHistory(input)); + } + + @Test + void saveAllergyHistory_savesEveryReportedAllergy() { + BenAllergyHistory input = mock(BenAllergyHistory.class); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenAllergyHistory())); + when(input.getBenAllergicHistory()).thenReturn(entries); + when(benAllergyHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1L, service.saveAllergyHistory(input)); + } + + @Test + void saveAllergyHistory_succeedsWhenNoAllergiesWereReported() { + BenAllergyHistory input = mock(BenAllergyHistory.class); + when(input.getBenAllergicHistory()).thenReturn(new ArrayList<>()); + assertEquals(1L, service.saveAllergyHistory(input)); + } + + @Test + void saveChildOptionalVaccineDetail_savesEveryOptionalVaccine() { + WrapperChildOptionalVaccineDetail wrapper = mock(WrapperChildOptionalVaccineDetail.class); + ArrayList entries = new ArrayList<>( + Collections.singletonList(new ChildOptionalVaccineDetail())); + when(wrapper.getChildOptionalVaccineDetails()).thenReturn(entries); + when(childOptionalVaccineDetailRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1L, service.saveChildOptionalVaccineDetail(wrapper)); + } + + @Test + void saveChildOptionalVaccineDetail_succeedsWhenNoOptionalVaccineWasGiven() { + WrapperChildOptionalVaccineDetail wrapper = mock(WrapperChildOptionalVaccineDetail.class); + when(wrapper.getChildOptionalVaccineDetails()).thenReturn(new ArrayList<>()); + assertEquals(1L, service.saveChildOptionalVaccineDetail(wrapper)); + } + + @Test + void saveImmunizationHistory_returnsTheIdOfTheFirstStoredVaccine() { + WrapperImmunizationHistory wrapper = mock(WrapperImmunizationHistory.class); + ChildVaccineDetail1 stored = new ChildVaccineDetail1(); + stored.setID(3L); + ArrayList entries = new ArrayList<>(Collections.singletonList(stored)); + when(wrapper.getBenChildVaccineDetails()).thenReturn(entries); + when(childVaccineDetail1Repo.saveAll(entries)).thenReturn(entries); + + assertEquals(3L, service.saveImmunizationHistory(wrapper)); + } + + @Test + void saveImmunizationHistory_returnsNullWhenNoVaccineWasStored() { + WrapperImmunizationHistory wrapper = mock(WrapperImmunizationHistory.class); + ArrayList entries = new ArrayList<>(); + when(wrapper.getBenChildVaccineDetails()).thenReturn(entries); + when(childVaccineDetail1Repo.saveAll(entries)).thenReturn(entries); + + assertNull(service.saveImmunizationHistory(wrapper)); + } + } + + @Nested + @DisplayName("vitals and anthropometry") + class Vitals { + + @Test + void saveBeneficiaryPhysicalAnthropometryDetails_returnsTheStoredId() { + BenAnthropometryDetail detail = new BenAnthropometryDetail(); + BenAnthropometryDetail stored = new BenAnthropometryDetail(); + stored.setID(5L); + when(benAnthropometryRepo.save(detail)).thenReturn(stored); + assertEquals(5L, service.saveBeneficiaryPhysicalAnthropometryDetails(detail)); + } + + @Test + void saveBeneficiaryPhysicalVitalDetails_averagesEveryBloodPressureReadingTaken() { + BenPhysicalVitalDetail detail = new BenPhysicalVitalDetail(); + detail.setSystolicBP_1stReading((short) 120); + detail.setDiastolicBP_1stReading((short) 80); + detail.setSystolicBP_2ndReading((short) 130); + detail.setDiastolicBP_2ndReading((short) 90); + detail.setSystolicBP_3rdReading((short) 140); + detail.setDiastolicBP_3rdReading((short) 100); + + BenPhysicalVitalDetail stored = new BenPhysicalVitalDetail(); + stored.setID(8L); + when(benPhysicalVitalRepo.save(detail)).thenReturn(stored); + + assertEquals(8L, service.saveBeneficiaryPhysicalVitalDetails(detail)); + assertEquals((short) 130, detail.getAverageSystolicBP()); + assertEquals((short) 90, detail.getAverageDiastolicBP()); + } + + @Test + void saveBeneficiaryPhysicalVitalDetails_leavesTheAverageUnsetWhenNoReadingWasTaken() { + BenPhysicalVitalDetail detail = new BenPhysicalVitalDetail(); + when(benPhysicalVitalRepo.save(detail)).thenReturn(null); + + assertNull(service.saveBeneficiaryPhysicalVitalDetails(detail)); + assertNull(detail.getAverageSystolicBP()); + } + + @Test + void getBeneficiaryPhysicalAnthropometryDetails_serialisesTheStoredRow() { + BenAnthropometryDetail stored = new BenAnthropometryDetail(); + stored.setID(1L); + when(benAnthropometryRepo.getBenAnthropometryDetail(1L, 2L)).thenReturn(stored); + assertTrue(service.getBeneficiaryPhysicalAnthropometryDetails(1L, 2L).contains("\"ID\":1")); + } + + @Test + void getBeneficiaryPhysicalVitalDetails_serialisesTheStoredRow() { + BenPhysicalVitalDetail stored = new BenPhysicalVitalDetail(); + stored.setID(2L); + when(benPhysicalVitalRepo.getBenPhysicalVitalDetail(1L, 2L)).thenReturn(stored); + assertTrue(service.getBeneficiaryPhysicalVitalDetails(1L, 2L).contains("\"ID\":2")); + } + + @Test + void updateANCAnthropometryDetails_marksAnAlreadySyncedRowAsUpdated() { + BenAnthropometryDetail detail = new BenAnthropometryDetail(); + detail.setBeneficiaryRegID(1L); + detail.setVisitCode(2L); + when(benAnthropometryRepo.getBenAnthropometryStatus(1L, 2L)).thenReturn("P"); + when(benAnthropometryRepo.updateANCCareDetails(any(), any(), any(), any(), any(), any(), any(), any(), + any(), org.mockito.ArgumentMatchers.eq("U"), anyLong(), anyLong())).thenReturn(1); + + assertEquals(1, service.updateANCAnthropometryDetails(detail)); + } + + @Test + void updateANCAnthropometryDetails_keepsARowThatWasNeverSyncedAsNew() { + BenAnthropometryDetail detail = new BenAnthropometryDetail(); + detail.setBeneficiaryRegID(1L); + detail.setVisitCode(2L); + when(benAnthropometryRepo.getBenAnthropometryStatus(1L, 2L)).thenReturn("N"); + when(benAnthropometryRepo.updateANCCareDetails(any(), any(), any(), any(), any(), any(), any(), any(), + any(), org.mockito.ArgumentMatchers.eq("N"), anyLong(), anyLong())).thenReturn(1); + + assertEquals(1, service.updateANCAnthropometryDetails(detail)); + } + + @Test + void updateANCAnthropometryDetails_doesNothingWithoutARow() { + assertEquals(0, service.updateANCAnthropometryDetails(null)); + } + + @Test + void updateANCPhysicalVitalDetails_copiesTheFirstReadingIntoTheAverage() { + BenPhysicalVitalDetail detail = new BenPhysicalVitalDetail(); + detail.setBeneficiaryRegID(1L); + detail.setVisitCode(2L); + detail.setSystolicBP_1stReading((short) 118); + detail.setDiastolicBP_1stReading((short) 76); + when(benPhysicalVitalRepo.getBenPhysicalVitalStatus(1L, 2L)).thenReturn("P"); + when(benPhysicalVitalRepo.updatePhysicalVitalDetails(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + anyString(), any(), any(), any(), anyLong(), anyLong())).thenReturn(1); + + assertEquals(1, service.updateANCPhysicalVitalDetails(detail)); + assertEquals((short) 118, detail.getAverageSystolicBP()); + assertEquals((short) 76, detail.getAverageDiastolicBP()); + } + + @Test + void updateANCPhysicalVitalDetails_doesNothingWithoutARow() { + assertEquals(0, service.updateANCPhysicalVitalDetails(null)); + } + + @Test + void saveIDRS_returnsTheStoredId() { + IDRSData data = new IDRSData(); + IDRSData stored = new IDRSData(); + stored.setId(4L); + when(iDRSDataRepo.save(data)).thenReturn(stored); + assertEquals(4L, service.saveIDRS(data)); + } + + @Test + void saveIDRS_returnsNullWhenNothingWasStored() { + IDRSData data = new IDRSData(); + when(iDRSDataRepo.save(data)).thenReturn(null); + assertNull(service.saveIDRS(data)); + } + + @Test + void savePhysicalActivity_returnsTheStoredId() { + PhysicalActivityType activity = new PhysicalActivityType(); + PhysicalActivityType stored = new PhysicalActivityType(); + stored.setpAID(6L); + when(physicalActivityTypeRepo.save(activity)).thenReturn(stored); + assertEquals(6L, service.savePhysicalActivity(activity)); + } + + @Test + void savePhysicalActivity_returnsNullWhenNothingWasStored() { + PhysicalActivityType activity = new PhysicalActivityType(); + when(physicalActivityTypeRepo.save(activity)).thenReturn(null); + assertNull(service.savePhysicalActivity(activity)); + } + } + + @Nested + @DisplayName("examination saves") + class ExaminationSaves { + + @Test + void savePhyGeneralExamination_joinsTheReportedDangerSigns() { + PhyGeneralExamination examination = new PhyGeneralExamination(); + examination.setTypeOfDangerSigns(new ArrayList<>(Arrays.asList("fever", "bleeding"))); + PhyGeneralExamination stored = new PhyGeneralExamination(); + stored.setID(1L); + when(phyGeneralExaminationRepo.save(examination)).thenReturn(stored); + + assertEquals(1L, service.savePhyGeneralExamination(examination)); + assertEquals("fever,bleeding,", examination.getTypeOfDangerSign()); + } + + @Test + void savePhyGeneralExamination_leavesDangerSignsUnsetWhenNoneWereReported() { + PhyGeneralExamination examination = new PhyGeneralExamination(); + when(phyGeneralExaminationRepo.save(examination)).thenReturn(null); + assertNull(service.savePhyGeneralExamination(examination)); + assertNull(examination.getTypeOfDangerSign()); + } + + @Test + void savePhyHeadToToeExamination_returnsTheStoredId() { + PhyHeadToToeExamination examination = new PhyHeadToToeExamination(); + PhyHeadToToeExamination stored = new PhyHeadToToeExamination(); + stored.setID(2L); + when(phyHeadToToeExaminationRepo.save(examination)).thenReturn(stored); + assertEquals(2L, service.savePhyHeadToToeExamination(examination)); + + when(phyHeadToToeExaminationRepo.save(examination)).thenReturn(null); + assertNull(service.savePhyHeadToToeExamination(examination)); + } + + @Test + void saveSysGastrointestinalExamination_returnsTheStoredId() { + SysGastrointestinalExamination examination = new SysGastrointestinalExamination(); + SysGastrointestinalExamination stored = new SysGastrointestinalExamination(); + stored.setID(3L); + when(sysGastrointestinalExaminationRepo.save(examination)).thenReturn(stored); + assertEquals(3L, service.saveSysGastrointestinalExamination(examination)); + + when(sysGastrointestinalExaminationRepo.save(examination)).thenReturn(null); + assertNull(service.saveSysGastrointestinalExamination(examination)); + } + + @Test + void saveSysCardiovascularExamination_returnsTheStoredId() { + SysCardiovascularExamination examination = new SysCardiovascularExamination(); + SysCardiovascularExamination stored = new SysCardiovascularExamination(); + stored.setID(4L); + when(sysCardiovascularExaminationRepo.save(examination)).thenReturn(stored); + assertEquals(4L, service.saveSysCardiovascularExamination(examination)); + + when(sysCardiovascularExaminationRepo.save(examination)).thenReturn(null); + assertNull(service.saveSysCardiovascularExamination(examination)); + } + + @Test + void saveSysRespiratoryExamination_returnsTheStoredId() { + SysRespiratoryExamination examination = new SysRespiratoryExamination(); + SysRespiratoryExamination stored = new SysRespiratoryExamination(); + stored.setID(5L); + when(sysRespiratoryExaminationRepo.save(examination)).thenReturn(stored); + assertEquals(5L, service.saveSysRespiratoryExamination(examination)); + + when(sysRespiratoryExaminationRepo.save(examination)).thenReturn(null); + assertNull(service.saveSysRespiratoryExamination(examination)); + } + + @Test + void saveSysCentralNervousExamination_returnsTheStoredId() { + SysCentralNervousExamination examination = new SysCentralNervousExamination(); + SysCentralNervousExamination stored = new SysCentralNervousExamination(); + stored.setID(6L); + when(sysCentralNervousExaminationRepo.save(examination)).thenReturn(stored); + assertEquals(6L, service.saveSysCentralNervousExamination(examination)); + + when(sysCentralNervousExaminationRepo.save(examination)).thenReturn(null); + assertNull(service.saveSysCentralNervousExamination(examination)); + } + + @Test + void saveSysMusculoskeletalSystemExamination_returnsTheStoredId() { + SysMusculoskeletalSystemExamination examination = new SysMusculoskeletalSystemExamination(); + SysMusculoskeletalSystemExamination stored = new SysMusculoskeletalSystemExamination(); + stored.setID(7L); + when(sysMusculoskeletalSystemExaminationRepo.save(examination)).thenReturn(stored); + assertEquals(7L, service.saveSysMusculoskeletalSystemExamination(examination)); + + when(sysMusculoskeletalSystemExaminationRepo.save(examination)).thenReturn(null); + assertNull(service.saveSysMusculoskeletalSystemExamination(examination)); + } + + @Test + void saveSysGenitourinarySystemExamination_returnsTheStoredId() { + SysGenitourinarySystemExamination examination = new SysGenitourinarySystemExamination(); + SysGenitourinarySystemExamination stored = new SysGenitourinarySystemExamination(); + stored.setID(8L); + when(sysGenitourinarySystemExaminationRepo.save(examination)).thenReturn(stored); + assertEquals(8L, service.saveSysGenitourinarySystemExamination(examination)); + + when(sysGenitourinarySystemExaminationRepo.save(examination)).thenReturn(null); + assertNull(service.saveSysGenitourinarySystemExamination(examination)); + } + } + + @Nested + @DisplayName("beneficiary history tables") + class HistoryTables { + + /** A stored row wide enough for any of the history mappers, with no values set. */ + private ArrayList oneEmptyRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[20]); + return rows; + } + + private void assertTableWithOneRow(String json) { + assertTrue(json.contains("\"columns\""), "the table must describe its columns: " + json); + assertTrue(json.contains("\"data\":[{"), "the stored row must be mapped into data: " + json); + } + + private void assertEmptyTable(String json) { + assertTrue(json.contains("\"columns\""), "the table must describe its columns even when empty: " + json); + assertTrue(json.contains("\"data\":[]"), "an empty result must map to no data: " + json); + } + + @Test + void fetchBenPastMedicalHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benMedHistoryRepo.getBenPastHistory(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenPastMedicalHistory(1L)); + + when(benMedHistoryRepo.getBenPastHistory(2L)).thenReturn(new ArrayList<>()); + assertEmptyTable(service.fetchBenPastMedicalHistory(2L)); + + when(benMedHistoryRepo.getBenPastHistory(3L)).thenReturn(null); + assertEmptyTable(service.fetchBenPastMedicalHistory(3L)); + } + + @Test + void fetchBenPersonalTobaccoHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benPersonalHabitRepo.getBenPersonalTobaccoHabitDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenPersonalTobaccoHistory(1L)); + + when(benPersonalHabitRepo.getBenPersonalTobaccoHabitDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenPersonalTobaccoHistory(2L)); + } + + @Test + void fetchBenPersonalAlcoholHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benPersonalHabitRepo.getBenPersonalAlcoholHabitDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenPersonalAlcoholHistory(1L)); + + when(benPersonalHabitRepo.getBenPersonalAlcoholHabitDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenPersonalAlcoholHistory(2L)); + } + + @Test + void fetchBenPersonalAllergyHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benAllergyHistoryRepo.getBenPersonalAllergyDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenPersonalAllergyHistory(1L)); + + when(benAllergyHistoryRepo.getBenPersonalAllergyDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenPersonalAllergyHistory(2L)); + } + + @Test + void fetchBenPersonalMedicationHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benMedicationHistoryRepo.getBenMedicationHistoryDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenPersonalMedicationHistory(1L)); + + when(benMedicationHistoryRepo.getBenMedicationHistoryDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenPersonalMedicationHistory(2L)); + } + + @Test + void fetchBenPersonalFamilyHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benFamilyHistoryRepo.getBenFamilyHistoryDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenPersonalFamilyHistory(1L)); + + when(benFamilyHistoryRepo.getBenFamilyHistoryDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenPersonalFamilyHistory(2L)); + } + + @Test + void fetchBenPhysicalHistory_mapsStoredRowsAndTolratesNoHistory() { + when(physicalActivityTypeRepo.getBenPhysicalHistoryDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenPhysicalHistory(1L)); + + when(physicalActivityTypeRepo.getBenPhysicalHistoryDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenPhysicalHistory(2L)); + } + + @Test + void fetchBenMenstrualHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benMenstrualDetailsRepo.getBenMenstrualDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenMenstrualHistory(1L)); + + when(benMenstrualDetailsRepo.getBenMenstrualDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenMenstrualHistory(2L)); + } + + @Test + void fetchBenPastObstetricHistory_mapsStoredRowsAndTolratesNoHistory() { + when(femaleObstetricHistoryRepo.getBenFemaleObstetricHistoryDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenPastObstetricHistory(1L)); + + when(femaleObstetricHistoryRepo.getBenFemaleObstetricHistoryDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenPastObstetricHistory(2L)); + } + + @Test + void fetchBenComorbidityHistory_mapsStoredRowsAndTolratesNoHistory() { + when(bencomrbidityCondRepo.getBencomrbidityCondDetails(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenComorbidityHistory(1L)); + + when(bencomrbidityCondRepo.getBencomrbidityCondDetails(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenComorbidityHistory(2L)); + } + + @Test + void fetchBenImmunizationHistory_mapsStoredRowsAndTolratesNoHistory() { + when(childVaccineDetail1Repo.getBenChildVaccineDetails(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenImmunizationHistory(1L)); + + when(childVaccineDetail1Repo.getBenChildVaccineDetails(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenImmunizationHistory(2L)); + } + + @Test + void fetchBenOptionalVaccineHistory_mapsStoredRowsAndTolratesNoHistory() { + when(childOptionalVaccineDetailRepo.getBenOptionalVaccineDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenOptionalVaccineHistory(1L)); + + when(childOptionalVaccineDetailRepo.getBenOptionalVaccineDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenOptionalVaccineHistory(2L)); + } + + @Test + void fetchBenPerinatalHistory_mapsStoredRowsAndTolratesNoHistory() { + when(perinatalHistoryRepo.getBenPerinatalDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenPerinatalHistory(1L)); + + when(perinatalHistoryRepo.getBenPerinatalDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenPerinatalHistory(2L)); + } + + @Test + void fetchBenFeedingHistory_mapsStoredRowsAndTolratesNoHistory() { + when(childFeedingDetailsRepo.getBenFeedingHistoryDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenFeedingHistory(1L)); + + when(childFeedingDetailsRepo.getBenFeedingHistoryDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenFeedingHistory(2L)); + } + + @Test + void fetchBenDevelopmentHistory_mapsStoredRowsAndTolratesNoHistory() { + when(benChildDevelopmentHistoryRepo.getBenDevelopmentHistoryDetail(1L)).thenReturn(oneEmptyRow()); + assertTableWithOneRow(service.fetchBenDevelopmentHistory(1L)); + + when(benChildDevelopmentHistoryRepo.getBenDevelopmentHistoryDetail(2L)).thenReturn(null); + assertEmptyTable(service.fetchBenDevelopmentHistory(2L)); + } + } + + @Nested + @DisplayName("history updates") + class HistoryUpdates { + + /** The (id, processed) pairs the update methods read before deleting the old rows. */ + private ArrayList statusRows(Object id, String processed) { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[] { id, processed }); + return rows; + } + + @Test + void updateBenChiefComplaints_replacesTheExistingComplaints() { + BenChiefComplaint complaint = new BenChiefComplaint(); + complaint.setBeneficiaryRegID(1L); + complaint.setVisitCode(2L); + complaint.setBenChiefComplaintID(3L); + List complaints = Collections.singletonList(complaint); + when(benChiefComplaintRepo.saveAll(complaints)).thenReturn(complaints); + + assertEquals(1, service.updateBenChiefComplaints(complaints)); + verify(benChiefComplaintRepo).deleteExistingBenChiefComplaints(1L, 2L); + verify(benChiefComplaintRepo).updateVanSerialNo(3L); + } + + @Test + void updateBenChiefComplaints_doesNothingWhenNoComplaintWasSent() { + assertEquals(0, service.updateBenChiefComplaints(null)); + assertEquals(0, service.updateBenChiefComplaints(new ArrayList<>())); + } + + @Test + void updateBenChiefComplaints_reportsFailureWhenNothingWasStored() { + BenChiefComplaint complaint = new BenChiefComplaint(); + List complaints = Collections.singletonList(complaint); + when(benChiefComplaintRepo.saveAll(complaints)).thenReturn(new ArrayList<>()); + assertEquals(0, service.updateBenChiefComplaints(complaints)); + } + + @Test + void updateBenPastHistoryDetails_marksAlreadySyncedRowsAsUpdatedBeforeReplacingThem() throws Exception { + BenMedHistory history = mock(BenMedHistory.class); + when(history.getBeneficiaryRegID()).thenReturn(1L); + when(history.getVisitCode()).thenReturn(2L); + when(benMedHistoryRepo.getBenMedHistoryStatus(1L, 2L)).thenReturn(statusRows(5L, "P")); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenMedHistory())); + when(history.getBenPastHistory()).thenReturn(entries); + when(benMedHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1, service.updateBenPastHistoryDetails(history)); + verify(benMedHistoryRepo).deleteExistingBenMedHistory(5L, "U"); + } + + @Test + void updateBenPastHistoryDetails_keepsNeverSyncedRowsMarkedAsNew() throws Exception { + BenMedHistory history = mock(BenMedHistory.class); + when(benMedHistoryRepo.getBenMedHistoryStatus(any(), any())).thenReturn(statusRows(5L, "N")); + when(history.getBenPastHistory()).thenReturn(new ArrayList<>()); + + assertEquals(1, service.updateBenPastHistoryDetails(history)); + verify(benMedHistoryRepo).deleteExistingBenMedHistory(5L, "N"); + } + + @Test + void updateBenPastHistoryDetails_doesNothingWithoutAHistory() throws Exception { + assertEquals(0, service.updateBenPastHistoryDetails(null)); + } + + @Test + void updateBenComorbidConditions_replacesTheStoredConditions() { + WrapperComorbidCondDetails wrapper = mock(WrapperComorbidCondDetails.class); + when(bencomrbidityCondRepo.getBenComrbidityCondHistoryStatus(any(), any())) + .thenReturn(statusRows(6L, "P")); + ArrayList entries = new ArrayList<>( + Collections.singletonList(new BencomrbidityCondDetails())); + when(wrapper.getComrbidityConds()).thenReturn(entries); + when(bencomrbidityCondRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1, service.updateBenComorbidConditions(wrapper)); + verify(bencomrbidityCondRepo).deleteExistingBenComrbidityCondDetails(6L, "U"); + } + + @Test + void updateBenComorbidConditions_succeedsWhenEveryConditionWasCleared() { + WrapperComorbidCondDetails wrapper = mock(WrapperComorbidCondDetails.class); + when(bencomrbidityCondRepo.getBenComrbidityCondHistoryStatus(any(), any())) + .thenReturn(statusRows(6L, null)); + when(wrapper.getComrbidityConds()).thenReturn(new ArrayList<>()); + + assertEquals(1, service.updateBenComorbidConditions(wrapper)); + verify(bencomrbidityCondRepo).deleteExistingBenComrbidityCondDetails(6L, "N"); + } + + @Test + void updateBenComorbidConditions_doesNothingWithoutConditions() { + assertEquals(0, service.updateBenComorbidConditions(null)); + } + + @Test + void updateBenMedicationHistory_replacesTheStoredMedication() { + WrapperMedicationHistory wrapper = mock(WrapperMedicationHistory.class); + when(benMedicationHistoryRepo.getBenMedicationHistoryStatus(any(), any())) + .thenReturn(statusRows(7L, "P")); + ArrayList entries = new ArrayList<>( + Collections.singletonList(new BenMedicationHistory())); + when(wrapper.getBenMedicationHistoryDetails()).thenReturn(entries); + when(benMedicationHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1, service.updateBenMedicationHistory(wrapper)); + verify(benMedicationHistoryRepo).deleteExistingBenMedicationHistory(7L, "U"); + } + + @Test + void updateBenMedicationHistory_succeedsWhenEveryMedicationWasCleared() { + WrapperMedicationHistory wrapper = mock(WrapperMedicationHistory.class); + when(benMedicationHistoryRepo.getBenMedicationHistoryStatus(any(), any())) + .thenReturn(statusRows(7L, "N")); + when(wrapper.getBenMedicationHistoryDetails()).thenReturn(new ArrayList<>()); + assertEquals(1, service.updateBenMedicationHistory(wrapper)); + } + + @Test + void updateBenMedicationHistory_doesNothingWithoutMedication() { + assertEquals(0, service.updateBenMedicationHistory(null)); + } + + @Test + void updateBenPersonalHistory_replacesTheStoredHabits() { + BenPersonalHabit habit = mock(BenPersonalHabit.class); + when(benPersonalHabitRepo.getBenPersonalHistoryStatus(any(), any())) + .thenReturn(statusRows(Integer.valueOf(8), "P")); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenPersonalHabit())); + when(habit.getPersonalHistory()).thenReturn(entries); + when(benPersonalHabitRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1, service.updateBenPersonalHistory(habit)); + verify(benPersonalHabitRepo).deleteExistingBenPersonalHistory(8, "U"); + } + + @Test + void updateBenPersonalHistory_succeedsWhenEveryHabitWasCleared() { + BenPersonalHabit habit = mock(BenPersonalHabit.class); + when(benPersonalHabitRepo.getBenPersonalHistoryStatus(any(), any())) + .thenReturn(statusRows(Integer.valueOf(8), "N")); + when(habit.getPersonalHistory()).thenReturn(new ArrayList<>()); + assertEquals(1, service.updateBenPersonalHistory(habit)); + } + + @Test + void updateBenPersonalHistory_doesNothingWithoutHabits() { + assertEquals(0, service.updateBenPersonalHistory(null)); + } + + @Test + void updateBenAllergicHistory_replacesTheStoredAllergies() { + BenAllergyHistory allergy = mock(BenAllergyHistory.class); + when(benAllergyHistoryRepo.getBenAllergyHistoryStatus(any(), any())).thenReturn(statusRows(9L, "P")); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenAllergyHistory())); + when(allergy.getBenAllergicHistory()).thenReturn(entries); + when(benAllergyHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1, service.updateBenAllergicHistory(allergy)); + verify(benAllergyHistoryRepo).deleteExistingBenAllergyHistory(9L, "U"); + } + + @Test + void updateBenAllergicHistory_succeedsWhenEveryAllergyWasCleared() { + BenAllergyHistory allergy = mock(BenAllergyHistory.class); + when(benAllergyHistoryRepo.getBenAllergyHistoryStatus(any(), any())).thenReturn(statusRows(9L, "N")); + when(allergy.getBenAllergicHistory()).thenReturn(new ArrayList<>()); + assertEquals(1, service.updateBenAllergicHistory(allergy)); + } + + @Test + void updateBenAllergicHistory_doesNothingWithoutAllergies() { + assertEquals(0, service.updateBenAllergicHistory(null)); + } + + @Test + void updateBenFamilyHistory_replacesTheStoredFamilyDiseases() { + BenFamilyHistory family = mock(BenFamilyHistory.class); + when(benFamilyHistoryRepo.getBenFamilyHistoryStatus(any(), any())).thenReturn(statusRows(10L, "P")); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenFamilyHistory())); + when(family.getBenFamilyHistory()).thenReturn(entries); + when(benFamilyHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1, service.updateBenFamilyHistory(family)); + verify(benFamilyHistoryRepo).deleteExistingBenFamilyHistory(10L, "U"); + } + + @Test + void updateBenFamilyHistory_succeedsWhenEveryDiseaseWasCleared() { + BenFamilyHistory family = mock(BenFamilyHistory.class); + when(benFamilyHistoryRepo.getBenFamilyHistoryStatus(any(), any())).thenReturn(statusRows(10L, "N")); + when(family.getBenFamilyHistory()).thenReturn(new ArrayList<>()); + assertEquals(1, service.updateBenFamilyHistory(family)); + } + + @Test + void updateBenFamilyHistory_doesNothingWithoutFamilyHistory() { + assertEquals(0, service.updateBenFamilyHistory(null)); + } + + @Test + void updateMenstrualHistory_updatesTheExistingRowWhenOneIsAlreadyStored() { + BenMenstrualDetails details = new BenMenstrualDetails(); + details.setBeneficiaryRegID(1L); + details.setVisitCode(2L); + ArrayList> problems = new ArrayList<>(); + Map problem = new HashMap<>(); + problem.put("menstrualProblemID", 3); + problem.put("problemName", "cramps"); + problems.add(problem); + details.setMenstrualProblemList(problems); + + when(benMenstrualDetailsRepo.getBenMenstrualDetailStatus(1L, 2L)).thenReturn("P"); + when(benMenstrualDetailsRepo.updateMenstrualDetails(any(), any(), any(), any(), any(), any(), any(), + anyString(), anyString(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), anyLong(), anyLong())) + .thenReturn(1); + + assertEquals(1, service.updateMenstrualHistory(details)); + assertEquals("3", details.getMenstrualProblemID()); + assertEquals("cramps", details.getProblemName()); + } + + @Test + void updateMenstrualHistory_insertsAFreshRowWhenNoneIsStoredYet() { + BenMenstrualDetails details = new BenMenstrualDetails(); + details.setModifiedBy("nurse"); + when(benMenstrualDetailsRepo.getBenMenstrualDetailStatus(any(), any())).thenReturn(null); + + BenMenstrualDetails stored = new BenMenstrualDetails(); + stored.setBenMenstrualID(4); + when(benMenstrualDetailsRepo.save(details)).thenReturn(stored); + + assertEquals(1, service.updateMenstrualHistory(details)); + assertEquals("nurse", details.getCreatedBy()); + } + + @Test + void updateMenstrualHistory_reportsFailureWhenTheFreshRowWasNotStored() { + BenMenstrualDetails details = new BenMenstrualDetails(); + when(benMenstrualDetailsRepo.getBenMenstrualDetailStatus(any(), any())).thenReturn(null); + BenMenstrualDetails stored = new BenMenstrualDetails(); + stored.setBenMenstrualID(0); + when(benMenstrualDetailsRepo.save(details)).thenReturn(stored); + + assertEquals(0, service.updateMenstrualHistory(details)); + } + + @Test + void updateMenstrualHistory_doesNothingWithoutMenstrualDetails() { + assertEquals(0, service.updateMenstrualHistory(null)); + } + + @Test + void updatePastObstetricHistory_replacesTheStoredPregnancies() { + WrapperFemaleObstetricHistory wrapper = mock(WrapperFemaleObstetricHistory.class); + when(femaleObstetricHistoryRepo.getBenObstetricHistoryStatus(any(), any())) + .thenReturn(statusRows(11L, "P")); + ArrayList entries = new ArrayList<>( + Collections.singletonList(new FemaleObstetricHistory())); + when(wrapper.getFemaleObstetricHistoryDetails()).thenReturn(entries); + when(femaleObstetricHistoryRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1, service.updatePastObstetricHistory(wrapper)); + verify(femaleObstetricHistoryRepo).deleteExistingObstetricHistory(11L, "U"); + } + + @Test + void updatePastObstetricHistory_doesNothingWithoutObstetricHistory() { + assertEquals(0, service.updatePastObstetricHistory(null)); + } + + @Test + void updateChildOptionalVaccineDetail_replacesTheStoredOptionalVaccines() { + WrapperChildOptionalVaccineDetail wrapper = mock(WrapperChildOptionalVaccineDetail.class); + when(childOptionalVaccineDetailRepo.getBenChildOptionalVaccineHistoryStatus(any(), any())) + .thenReturn(statusRows(12L, "P")); + ArrayList entries = new ArrayList<>( + Collections.singletonList(new ChildOptionalVaccineDetail())); + when(wrapper.getChildOptionalVaccineDetails()).thenReturn(entries); + when(childOptionalVaccineDetailRepo.saveAll(entries)).thenReturn(entries); + + assertEquals(1, service.updateChildOptionalVaccineDetail(wrapper)); + verify(childOptionalVaccineDetailRepo).deleteExistingChildOptionalVaccineDetail(12L, "U"); + } + + @Test + void updateChildOptionalVaccineDetail_succeedsWhenEveryOptionalVaccineWasCleared() { + WrapperChildOptionalVaccineDetail wrapper = mock(WrapperChildOptionalVaccineDetail.class); + when(childOptionalVaccineDetailRepo.getBenChildOptionalVaccineHistoryStatus(any(), any())) + .thenReturn(statusRows(12L, "N")); + when(wrapper.getChildOptionalVaccineDetails()).thenReturn(new ArrayList<>()); + assertEquals(1, service.updateChildOptionalVaccineDetail(wrapper)); + } + + @Test + void updateChildOptionalVaccineDetail_doesNothingWithoutOptionalVaccines() { + assertEquals(0, service.updateChildOptionalVaccineDetail(null)); + } + + @Test + void updateChildImmunizationDetail_marksAPreviouslySyncedVaccineAsUpdated() { + ChildVaccineDetail1 vaccine = new ChildVaccineDetail1(); + vaccine.setBeneficiaryRegID(1L); + vaccine.setVisitCode(2L); + vaccine.setDefaultReceivingAge("6 weeks"); + vaccine.setVaccineName("BCG"); + + WrapperImmunizationHistory wrapper = mock(WrapperImmunizationHistory.class); + when(wrapper.getBenChildVaccineDetails()) + .thenReturn(new ArrayList<>(Collections.singletonList(vaccine))); + + ArrayList statuses = new ArrayList<>(); + statuses.add(new Object[] { "6 weeks", "BCG", "P" }); + when(childVaccineDetail1Repo.getBenChildVaccineDetailStatus(1L, 2L)).thenReturn(statuses); + when(childVaccineDetail1Repo.updateChildANCImmunization(any(), any(), org.mockito.ArgumentMatchers.eq("U"), + anyLong(), anyLong(), anyString(), anyString(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateChildImmunizationDetail(wrapper)); + } + + @Test + void updateChildImmunizationDetail_treatsAVaccineWithNoStoredStatusAsNew() { + ChildVaccineDetail1 vaccine = new ChildVaccineDetail1(); + vaccine.setBeneficiaryRegID(1L); + vaccine.setVisitCode(2L); + vaccine.setDefaultReceivingAge("6 weeks"); + vaccine.setVaccineName("BCG"); + + WrapperImmunizationHistory wrapper = mock(WrapperImmunizationHistory.class); + when(wrapper.getBenChildVaccineDetails()) + .thenReturn(new ArrayList<>(Collections.singletonList(vaccine))); + when(childVaccineDetail1Repo.getBenChildVaccineDetailStatus(1L, 2L)).thenReturn(new ArrayList<>()); + when(childVaccineDetail1Repo.updateChildANCImmunization(any(), any(), org.mockito.ArgumentMatchers.eq("N"), + anyLong(), anyLong(), anyString(), anyString(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateChildImmunizationDetail(wrapper)); + } + } + + @Nested + @DisplayName("examination updates") + class ExaminationUpdates { + + @Test + void updatePhyGeneralExamination_joinsDangerSignsAndMarksAnAlreadySyncedRow() { + PhyGeneralExamination examination = new PhyGeneralExamination(); + examination.setBeneficiaryRegID(1L); + examination.setVisitCode(2L); + examination.setTypeOfDangerSigns(new ArrayList<>(Arrays.asList("fever"))); + when(phyGeneralExaminationRepo.getBenGeneralExaminationStatus(1L, 2L)).thenReturn("P"); + when(phyGeneralExaminationRepo.updatePhyGeneralExamination(any(), any(), any(), any(), any(), any(), any(), + anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + any(), org.mockito.ArgumentMatchers.eq("U"), anyLong(), anyLong())).thenReturn(1); + + assertEquals(1, service.updatePhyGeneralExamination(examination)); + assertEquals("fever,", examination.getTypeOfDangerSign()); + } + + @Test + void updatePhyGeneralExamination_keepsANeverSyncedRowMarkedAsNew() { + PhyGeneralExamination examination = new PhyGeneralExamination(); + when(phyGeneralExaminationRepo.getBenGeneralExaminationStatus(any(), any())).thenReturn("N"); + when(phyGeneralExaminationRepo.updatePhyGeneralExamination(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + org.mockito.ArgumentMatchers.eq("N"), any(), any())).thenReturn(1); + + assertEquals(1, service.updatePhyGeneralExamination(examination)); + } + + @Test + void updatePhyGeneralExamination_doesNothingWithoutAnExamination() { + assertEquals(0, service.updatePhyGeneralExamination(null)); + } + + @Test + void updatePhyHeadToToeExamination_marksAnAlreadySyncedRowAsUpdated() { + PhyHeadToToeExamination examination = new PhyHeadToToeExamination(); + when(phyHeadToToeExaminationRepo.getBenHeadToToeExaminationStatus(any(), any())).thenReturn("P"); + when(phyHeadToToeExaminationRepo.updatePhyHeadToToeExamination(any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), + org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1); + + assertEquals(1, service.updatePhyHeadToToeExamination(examination)); + assertEquals(0, service.updatePhyHeadToToeExamination(null)); + } + + @Test + void updateSysCardiovascularExamination_marksAnAlreadySyncedRowAsUpdated() { + SysCardiovascularExamination examination = new SysCardiovascularExamination(); + when(sysCardiovascularExaminationRepo.getBenCardiovascularExaminationStatus(any(), any())) + .thenReturn("P"); + when(sysCardiovascularExaminationRepo.updateSysCardiovascularExamination(any(), any(), any(), any(), any(), + any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1); + + assertEquals(1, service.updateSysCardiovascularExamination(examination)); + assertEquals(0, service.updateSysCardiovascularExamination(null)); + } + + @Test + void updateSysRespiratoryExamination_marksAnAlreadySyncedRowAsUpdated() { + SysRespiratoryExamination examination = new SysRespiratoryExamination(); + when(sysRespiratoryExaminationRepo.getBenRespiratoryExaminationStatus(any(), any())).thenReturn("P"); + when(sysRespiratoryExaminationRepo.updateSysRespiratoryExamination(any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), + any())).thenReturn(1); + + assertEquals(1, service.updateSysRespiratoryExamination(examination)); + assertEquals(0, service.updateSysRespiratoryExamination(null)); + } + + @Test + void updateSysCentralNervousExamination_marksAnAlreadySyncedRowAsUpdated() { + SysCentralNervousExamination examination = new SysCentralNervousExamination(); + when(sysCentralNervousExaminationRepo.getBenCentralNervousExaminationStatus(any(), any())) + .thenReturn("P"); + when(sysCentralNervousExaminationRepo.updateSysCentralNervousExamination(any(), any(), any(), any(), any(), + any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1); + + assertEquals(1, service.updateSysCentralNervousExamination(examination)); + assertEquals(0, service.updateSysCentralNervousExamination(null)); + } + + @Test + void updateSysMusculoskeletalSystemExamination_marksAnAlreadySyncedRowAsUpdated() { + SysMusculoskeletalSystemExamination examination = new SysMusculoskeletalSystemExamination(); + when(sysMusculoskeletalSystemExaminationRepo.getBenMusculoskeletalSystemExaminationStatus(any(), any())) + .thenReturn("P"); + when(sysMusculoskeletalSystemExaminationRepo.updateSysMusculoskeletalSystemExamination(any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), + any())).thenReturn(1); + + assertEquals(1, service.updateSysMusculoskeletalSystemExamination(examination)); + assertEquals(0, service.updateSysMusculoskeletalSystemExamination(null)); + } + + @Test + void updateSysGenitourinarySystemExamination_marksAnAlreadySyncedRowAsUpdated() { + SysGenitourinarySystemExamination examination = new SysGenitourinarySystemExamination(); + when(sysGenitourinarySystemExaminationRepo.getBenGenitourinarySystemExaminationStatus(any(), any())) + .thenReturn("P"); + when(sysGenitourinarySystemExaminationRepo.updateSysGenitourinarySystemExamination(any(), any(), any(), + any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1); + + assertEquals(1, service.updateSysGenitourinarySystemExamination(examination)); + assertEquals(0, service.updateSysGenitourinarySystemExamination(null)); + } + + @Test + void updateSysGastrointestinalExamination_marksAnAlreadySyncedRowAsUpdated() { + SysGastrointestinalExamination examination = new SysGastrointestinalExamination(); + when(sysGastrointestinalExaminationRepo.getBenGastrointestinalExaminationStatus(any(), any())) + .thenReturn("P"); + when(sysGastrointestinalExaminationRepo.updateSysGastrointestinalExamination(any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), + any())).thenReturn(1); + + assertEquals(1, service.updateSysGastrointestinalExamination(examination)); + assertEquals(0, service.updateSysGastrointestinalExamination(null)); + } + } + + @Nested + @DisplayName("examination and history reads") + class Reads { + + private ArrayList oneEmptyRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[20]); + return rows; + } + + @Test + void getBenChiefComplaints_serialisesTheStoredComplaints() { + when(benChiefComplaintRepo.getBenChiefComplaints(1L, 2L)).thenReturn(new ArrayList<>()); + assertNotNull(service.getBenChiefComplaints(1L, 2L)); + } + + @Test + void getPastHistoryData_mapsTheStoredRows() { + when(benMedHistoryRepo.getBenPastHistory(1L, 2L)).thenReturn(oneEmptyRow()); + assertNotNull(service.getPastHistoryData(1L, 2L)); + + when(benMedHistoryRepo.getBenPastHistory(3L, 4L)).thenReturn(new ArrayList<>()); + assertNull(service.getPastHistoryData(3L, 4L)); + } + + @Test + void getComorbidityConditionsHistory_mapsTheStoredRows() { + when(bencomrbidityCondRepo.getBencomrbidityCondDetails(1L, 2L)).thenReturn(new ArrayList<>()); + assertNotNull(service.getComorbidityConditionsHistory(1L, 2L)); + } + + @Test + void getMedicationHistory_mapsTheStoredRows() { + when(benMedicationHistoryRepo.getBenMedicationHistoryDetail(1L, 2L)).thenReturn(new ArrayList<>()); + assertNotNull(service.getMedicationHistory(1L, 2L)); + } + + @Test + void getPersonalHistory_returnsAnEmptyHabitWhenNothingWasRecorded() { + when(benPersonalHabitRepo.getBenPersonalHabitDetail(1L, 2L)).thenReturn(new ArrayList<>()); + when(benAllergyHistoryRepo.getBenPersonalAllergyDetail(1L, 2L)).thenReturn(new ArrayList<>()); + assertNotNull(service.getPersonalHistory(1L, 2L)); + } + + @Test + void getPersonalHistory_copiesTheAllergyStatusOntoTheHabit() { + when(benPersonalHabitRepo.getBenPersonalHabitDetail(1L, 2L)).thenReturn(new ArrayList<>()); + when(benAllergyHistoryRepo.getBenPersonalAllergyDetail(1L, 2L)).thenReturn(oneEmptyRow()); + + BenPersonalHabit habit = service.getPersonalHistory(1L, 2L); + assertNotNull(habit.getAllergicList()); + } + + @Test + void getFamilyHistory_mapsTheStoredRows() { + when(benFamilyHistoryRepo.getBenFamilyHistoryDetail(1L, 2L)).thenReturn(oneEmptyRow()); + assertNotNull(service.getFamilyHistory(1L, 2L)); + } + + @Test + void getFamilyHistoryDetail_mapsTheScreeningVariantOfTheRows() { + when(benFamilyHistoryRepo.getBenFamilyHisDetail(1L, 2L)).thenReturn(oneEmptyRow()); + assertNotNull(service.getFamilyHistoryDetail(1L, 2L)); + } + + @Test + void getPhysicalActivityType_delegatesToRepo() { + PhysicalActivityType stored = new PhysicalActivityType(); + when(physicalActivityTypeRepo.getBenPhysicalHistoryDetails(1L, 2L)).thenReturn(stored); + assertEquals(stored, service.getPhysicalActivityType(1L, 2L)); + } + + @Test + void getBeneficiaryIdrsDetails_mapsTheStoredRows() { + when(iDRSDataRepo.getBenIdrsDetail(1L, 2L)).thenReturn(new ArrayList<>()); + service.getBeneficiaryIdrsDetails(1L, 2L); + } + + @Test + void getMenstrualHistory_splitsTheStoredProblemsBackIntoAList() { + BenMenstrualDetails stored = new BenMenstrualDetails(); + stored.setMenstrualProblemID("1,2"); + stored.setProblemName("cramps,spotting"); + try (org.mockito.MockedStatic statics = org.mockito.Mockito + .mockStatic(BenMenstrualDetails.class)) { + statics.when(() -> BenMenstrualDetails.getBenMenstrualDetails(any())).thenReturn(stored); + BenMenstrualDetails result = service.getMenstrualHistory(1L, 2L); + assertEquals(2, result.getMenstrualProblemList().size()); + assertEquals("cramps", result.getMenstrualProblemList().get(0).get("problemName")); + } + } + + @Test + void getMenstrualHistory_leavesTheProblemListUnsetWhenNoProblemWasRecorded() { + BenMenstrualDetails stored = new BenMenstrualDetails(); + try (org.mockito.MockedStatic statics = org.mockito.Mockito + .mockStatic(BenMenstrualDetails.class)) { + statics.when(() -> BenMenstrualDetails.getBenMenstrualDetails(any())).thenReturn(stored); + assertNull(service.getMenstrualHistory(1L, 2L).getMenstrualProblemList()); + } + } + + @Test + void getChildOptionalVaccineHistory_mapsTheStoredRows() { + when(childOptionalVaccineDetailRepo.getBenOptionalVaccineDetail(1L, 2L)).thenReturn(new ArrayList<>()); + service.getChildOptionalVaccineHistory(1L, 2L); + } + + @Test + void getImmunizationHistory_mapsTheStoredRows() { + when(childVaccineDetail1Repo.getBenChildVaccineDetails(1L, 2L)).thenReturn(new ArrayList<>()); + service.getImmunizationHistory(1L, 2L); + } + + @Test + void getGeneralExaminationData_splitsTheStoredDangerSignsBackIntoAList() { + PhyGeneralExamination stored = new PhyGeneralExamination(); + stored.setTypeOfDangerSign("fever,bleeding"); + when(phyGeneralExaminationRepo.getPhyGeneralExaminationData(1L, 2L)).thenReturn(stored); + + assertEquals(Arrays.asList("fever", "bleeding"), service.getGeneralExaminationData(1L, 2L) + .getTypeOfDangerSigns()); + } + + @Test + void getGeneralExaminationData_returnsAnEmptyDangerSignListWhenNoneWereRecorded() { + PhyGeneralExamination stored = new PhyGeneralExamination(); + when(phyGeneralExaminationRepo.getPhyGeneralExaminationData(1L, 2L)).thenReturn(stored); + assertTrue(service.getGeneralExaminationData(1L, 2L).getTypeOfDangerSigns().isEmpty()); + } + + @Test + void getGeneralExaminationData_returnsNullWhenNoExaminationWasRecorded() { + when(phyGeneralExaminationRepo.getPhyGeneralExaminationData(1L, 2L)).thenReturn(null); + assertNull(service.getGeneralExaminationData(1L, 2L)); + } + + @Test + void theRemainingSystemExaminationReadsDelegateToTheirRepositories() { + PhyHeadToToeExamination headToToe = new PhyHeadToToeExamination(); + when(phyHeadToToeExaminationRepo.getPhyHeadToToeExaminationData(1L, 2L)).thenReturn(headToToe); + assertEquals(headToToe, service.getHeadToToeExaminationData(1L, 2L)); + + SysGastrointestinalExamination gastro = new SysGastrointestinalExamination(); + when(sysGastrointestinalExaminationRepo.getSSysGastrointestinalExamination(1L, 2L)).thenReturn(gastro); + assertEquals(gastro, service.getSysGastrointestinalExamination(1L, 2L)); + + SysCardiovascularExamination cardio = new SysCardiovascularExamination(); + when(sysCardiovascularExaminationRepo.getSysCardiovascularExaminationData(1L, 2L)).thenReturn(cardio); + assertEquals(cardio, service.getCardiovascularExamination(1L, 2L)); + + SysRespiratoryExamination respiratory = new SysRespiratoryExamination(); + when(sysRespiratoryExaminationRepo.getSysRespiratoryExaminationData(1L, 2L)).thenReturn(respiratory); + assertEquals(respiratory, service.getRespiratoryExamination(1L, 2L)); + + SysCentralNervousExamination nervous = new SysCentralNervousExamination(); + when(sysCentralNervousExaminationRepo.getSysCentralNervousExaminationData(1L, 2L)).thenReturn(nervous); + assertEquals(nervous, service.getSysCentralNervousExamination(1L, 2L)); + + SysMusculoskeletalSystemExamination musculoskeletal = new SysMusculoskeletalSystemExamination(); + when(sysMusculoskeletalSystemExaminationRepo.getSysMusculoskeletalSystemExamination(1L, 2L)) + .thenReturn(musculoskeletal); + assertEquals(musculoskeletal, service.getMusculoskeletalExamination(1L, 2L)); + + SysGenitourinarySystemExamination genitourinary = new SysGenitourinarySystemExamination(); + when(sysGenitourinarySystemExaminationRepo.getSysGenitourinarySystemExaminationData(1L, 2L)) + .thenReturn(genitourinary); + assertEquals(genitourinary, service.getGenitourinaryExamination(1L, 2L)); + } + } + + @Nested + @DisplayName("prescriptions and prescribed drugs") + class Prescriptions { + + private com.iemr.mmu.data.snomedct.SCTDescription diagnosis(String term, String conceptId) { + com.iemr.mmu.data.snomedct.SCTDescription description = new com.iemr.mmu.data.snomedct.SCTDescription(); + description.setTerm(term); + description.setConceptID(conceptId); + return description; + } + + private com.iemr.mmu.data.quickConsultation.PrescriptionDetail storedPrescription(Long id) { + com.iemr.mmu.data.quickConsultation.PrescriptionDetail stored = + new com.iemr.mmu.data.quickConsultation.PrescriptionDetail(); + stored.setPrescriptionID(id); + return stored; + } + + @Test + void saveBenPrescription_joinsEveryProvisionalDiagnosisTermAndConceptId() { + com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription = + new com.iemr.mmu.data.quickConsultation.PrescriptionDetail(); + ArrayList diagnoses = new ArrayList<>(); + diagnoses.add(diagnosis("Fever", "111")); + diagnoses.add(diagnosis("Cough", null)); + diagnoses.add(diagnosis(null, "333")); + prescription.setProvisionalDiagnosisList(diagnoses); + when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(21L)); + + assertEquals(21L, service.saveBenPrescription(prescription)); + assertEquals("Fever || Cough", prescription.getDiagnosisProvided()); + assertEquals("111 || N/A", prescription.getDiagnosisProvided_SCTCode()); + verify(prescriptionDetailRepo).updateVanSerialNo(21L); + } + + @Test + void saveBenPrescription_leavesTheDiagnosisUnsetWhenNoneWasProvided() { + com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription = + new com.iemr.mmu.data.quickConsultation.PrescriptionDetail(); + when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(22L)); + + assertEquals(22L, service.saveBenPrescription(prescription)); + assertNull(prescription.getDiagnosisProvided()); + } + + @Test + void saveBenPrescription_returnsNullWhenTheRowWasNotPersisted() { + com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription = + new com.iemr.mmu.data.quickConsultation.PrescriptionDetail(); + when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(0L)); + assertNull(service.saveBenPrescription(prescription)); + } + + @Test + void savePrescriptionDetailsAndGetPrescriptionID_buildsThePrescriptionFromTheVisitContext() { + when(prescriptionDetailRepo.save(any())).thenReturn(storedPrescription(23L)); + ArrayList diagnoses = new ArrayList<>(); + diagnoses.add(diagnosis("Fever", "111")); + + assertEquals(23L, service.savePrescriptionDetailsAndGetPrescriptionID(1L, 2L, 3, "doctor", "x-ray", 4L, 5, + 6, diagnoses)); + assertEquals(23L, service.savePrescriptionDetailsAndGetPrescriptionID(1L, 2L, 3, "doctor", "x-ray", 4L, 5, + 6, null)); + } + + @Test + void savePrescriptionDetailsCovid19_recordsTheDoctorDiagnosisWhenOneWasGiven() { + when(prescriptionDetailRepo.save(any())).thenReturn(storedPrescription(24L)); + + assertEquals(24L, + service.savePrescriptionDetailsCovid19(1L, 2L, 3, "doctor", "x-ray", 4L, 5, 6, "Covid")); + assertEquals(24L, service.savePrescriptionDetailsCovid19(1L, 2L, 3, "doctor", "x-ray", 4L, 5, 6, null)); + } + + @Test + void saveBeneficiaryPrescription_readsThePrescriptionOutOfTheCaseSheet() throws Exception { + when(prescriptionDetailRepo.save(any())).thenReturn(storedPrescription(25L)); + com.google.gson.JsonObject caseSheet = new com.google.gson.JsonObject(); + caseSheet.addProperty("beneficiaryRegID", 1); + assertEquals(25L, service.saveBeneficiaryPrescription(caseSheet)); + } + + @Test + void updatePrescription_updatesTheStoredRowAndJoinsTheDiagnosisTerms() { + com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription = + new com.iemr.mmu.data.quickConsultation.PrescriptionDetail(); + prescription.setBeneficiaryRegID(1L); + prescription.setVisitCode(2L); + prescription.setPrescriptionID(3L); + ArrayList diagnoses = new ArrayList<>(); + diagnoses.add(diagnosis("Fever", null)); + diagnoses.add(diagnosis("Cough", "222")); + prescription.setProvisionalDiagnosisList(diagnoses); + + when(prescriptionDetailRepo.getGeneralOPDDiagnosisStatus(1L, 2L, 3L)).thenReturn("P"); + when(prescriptionDetailRepo.updatePrescription(anyString(), any(), any(), + org.mockito.ArgumentMatchers.eq("U"), anyLong(), anyLong(), anyLong(), any(), anyString(), any())) + .thenReturn(1); + + assertEquals(1, service.updatePrescription(prescription)); + assertEquals("Fever || Cough", prescription.getDiagnosisProvided()); + assertEquals("N/A || 222", prescription.getDiagnosisProvided_SCTCode()); + } + + @Test + void updatePrescription_insertsAFreshRowWhenNoneIsStoredYet() { + com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription = + new com.iemr.mmu.data.quickConsultation.PrescriptionDetail(); + when(prescriptionDetailRepo.getGeneralOPDDiagnosisStatus(any(), any(), any())).thenReturn(null); + when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(26L)); + + assertEquals(1, service.updatePrescription(prescription)); + } + + @Test + void updatePrescription_reportsFailureWhenTheFreshRowWasNotStored() { + com.iemr.mmu.data.quickConsultation.PrescriptionDetail prescription = + new com.iemr.mmu.data.quickConsultation.PrescriptionDetail(); + when(prescriptionDetailRepo.getGeneralOPDDiagnosisStatus(any(), any(), any())).thenReturn(null); + when(prescriptionDetailRepo.save(prescription)).thenReturn(storedPrescription(0L)); + + assertEquals(0, service.updatePrescription(prescription)); + } + + @Test + void saveBeneficiaryLabTestOrderDetails_succeedsWhenTheCaseSheetOrdersNoTest() { + assertEquals(1L, service.saveBeneficiaryLabTestOrderDetails(new com.google.gson.JsonObject(), 1L)); + } + + @Test + void saveBeneficiaryLabTestOrderDetails_storesEveryOrderedTest() { + com.google.gson.JsonObject caseSheet = new com.google.gson.JsonObject(); + com.google.gson.JsonArray orders = new com.google.gson.JsonArray(); + com.google.gson.JsonObject order = new com.google.gson.JsonObject(); + order.addProperty("testID", 1); + orders.add(order); + caseSheet.add("labTestOrders", orders); + + when(labTestOrderDetailRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + assertEquals(1L, service.saveBeneficiaryLabTestOrderDetails(caseSheet, 1L)); + } + + @Test + void saveBenPrescribedDrugsList_calculatesTheQuantityForTabletsAndCapsules() { + com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail tablet = drug("Tablet", "One Tab", + "Twice Daily(BD)", "5", "Day(s)"); + com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail syrup = drug("Syrup", "5 ml", "Once Daily(OD)", + "5", "Day(s)"); + tablet.setId(1L); + List drugs = Arrays.asList(tablet, syrup); + when(prescribedDrugDetailRepo.saveAll(drugs)).thenReturn(drugs); + + Map result = service.saveBenPrescribedDrugsList(drugs); + + assertEquals(2, result.get("count")); + assertEquals(Collections.singletonList(1L), result.get("prescribedDrugIDs")); + assertEquals(10, tablet.getQtyPrescribed()); + assertNull(syrup.getQtyPrescribed()); + } + + @Test + void saveBenPrescribedDrugsList_succeedsWhenNoDrugWasPrescribed() { + Map result = service.saveBenPrescribedDrugsList(new ArrayList<>()); + assertEquals(1, result.get("count")); + } + + @Test + void saveBenPrescribedDrugsList_reportsNothingSavedWhenTheStoreDropsARow() { + com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail tablet = drug("Tablet", "One Tab", + "Once Daily(OD)", "1", "Day(s)"); + List drugs = Collections.singletonList(tablet); + when(prescribedDrugDetailRepo.saveAll(drugs)).thenReturn(new ArrayList<>()); + + assertEquals(0, service.saveBenPrescribedDrugsList(drugs).get("count")); + } + + @org.junit.jupiter.params.ParameterizedTest(name = "{0} {1} {2} for {3} {4} is {5}") + @org.junit.jupiter.params.provider.CsvSource({ + "Tablet, Half Tab, Once Daily(OD), 2, Day(s), 1", + "Tablet, One Tab, Once Daily(OD) Before Food, 2, Day(s), 2", + "Tablet, One & Half Tab, Once Daily(OD) After Food, 2, Day(s), 3", + "Tablet, Two Tabs, Once Daily(OD) At Bedtime, 2, Day(s), 4", + "Capsule, One Cap, Once Daily(OD), 3, Day(s), 3", + "Tablet, Half Tab, Twice Daily(BD), 2, Day(s), 2", + "Tablet, One & Half Tab, Twice Daily(BD) Before Food, 1, Day(s), 3", + "Tablet, Two Tabs, Twice Daily(BD) After Food, 1, Day(s), 4", + "Capsule, One Cap, Twice Daily(BD), 1, Day(s), 2", + "Tablet, Half Tab, Thrice Daily (TID), 2, Day(s), 3", + "Tablet, One Tab, Thrice Daily (TID) After Food, 1, Day(s), 3", + "Tablet, One & Half Tab, Thrice Daily (TID) Before Food, 1, Day(s), 5", + "Tablet, Two Tabs, Thrice Daily (TID), 1, Day(s), 6", + "Capsule, One Cap, Thrice Daily (TID), 1, Day(s), 3", + "Tablet, Half Tab, Four Times in a Day (QID), 1, Day(s), 2", + "Tablet, One Tab, Four Times in a Day AF, 1, Day(s), 4", + "Tablet, One & Half Tab, Four Times in a Day BF, 1, Day(s), 6", + "Tablet, Two Tabs, Four Times in a Day (QID), 1, Day(s), 8", + "Capsule, One Cap, Four Times in a Day (QID), 1, Day(s), 4", + "Tablet, Half Tab, Single Dose, 5, Day(s), 1", + "Tablet, One Tab, Stat Dose, 5, Day(s), 1", + "Tablet, One & Half Tab, Single Dose Before Food, 1, Day(s), 2", + "Tablet, Two Tabs, Single Dose After Food, 1, Day(s), 2", + "Capsule, One Cap, Single Dose, 1, Day(s), 1", + "Tablet, Half Tab, Once in a Week, 4, Week(s), 2", + "Tablet, One Tab, Once in a Week After Food, 4, Week(s), 4", + "Tablet, One & Half Tab, Once in a Week Before Food, 4, Week(s), 6", + "Tablet, Two Tabs, Once in a Week, 4, Week(s), 8", + "Capsule, One Cap, Once in a Week, 4, Week(s), 5", + "Tablet, Half Tab, SOS, 2, Day(s), 1", + "Tablet, One Tab, SOS, 1, Month(s), 30", + "Tablet, One & Half Tab, SOS, 1, Day(s), 2", + "Tablet, Two Tabs, SOS, 1, Day(s), 2", + "Capsule, One Cap, SOS, 1, Day(s), 1", + "Tablet, Unknown Dose, Once Daily(OD), 1, Day(s), 0", + "Tablet, One Tab, Unrecognised Frequency, 1, Day(s), 0", + "Tablet, One Tab, Once Daily(OD), 1, Unknown Unit, 0" }) + void saveBenPrescribedDrugsList_derivesTheDispensedQuantityFromFormDoseAndFrequency(String form, String dose, + String frequency, String duration, String unit, int expectedQuantity) { + com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail drug = drug(form, dose, frequency, duration, + unit); + List drugs = Collections.singletonList(drug); + when(prescribedDrugDetailRepo.saveAll(drugs)).thenReturn(drugs); + + service.saveBenPrescribedDrugsList(drugs); + + assertEquals(expectedQuantity, drug.getQtyPrescribed()); + } + + @Test + void saveBenPrescribedDrugsList_leavesTheQuantityAtZeroWhenTheOrderIsIncomplete() { + com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail drug = drug("Tablet", null, "Once Daily(OD)", + "1", "Day(s)"); + List drugs = Collections.singletonList(drug); + when(prescribedDrugDetailRepo.saveAll(drugs)).thenReturn(drugs); + + service.saveBenPrescribedDrugsList(drugs); + + assertEquals(0, drug.getQtyPrescribed()); + } + + private com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail drug(String form, String dose, + String frequency, String duration, String unit) { + com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail drug = + new com.iemr.mmu.data.quickConsultation.PrescribedDrugDetail(); + drug.setFormName(form); + drug.setDose(dose); + drug.setFrequency(frequency); + drug.setDuration(duration); + drug.setUnit(unit); + return drug; + } + } + + @Nested + @DisplayName("investigations, worklists and status flags") + class WorklistsAndInvestigations { + + @Test + void saveBenInvestigationDetails_storesThePrescriptionAndItsInvestigations() { + com.iemr.mmu.data.anc.WrapperBenInvestigationANC wrapper = + new com.iemr.mmu.data.anc.WrapperBenInvestigationANC(); + wrapper.setBeneficiaryRegID(1L); + com.iemr.mmu.data.quickConsultation.PrescriptionDetail stored = + new com.iemr.mmu.data.quickConsultation.PrescriptionDetail(); + stored.setPrescriptionID(30L); + when(prescriptionDetailRepo.save(any())).thenReturn(stored); + + assertEquals(1, service.saveBenInvestigationDetails(wrapper)); + assertEquals(30L, wrapper.getPrescriptionID()); + } + + @Test + void saveBenInvestigationDetails_doesNothingWithoutInvestigationData() { + assertEquals(0, service.saveBenInvestigationDetails(null)); + } + + @Test + void saveBenInvestigation_copiesTheVisitContextOntoEveryOrderedTest() { + com.iemr.mmu.data.anc.WrapperBenInvestigationANC wrapper = + new com.iemr.mmu.data.anc.WrapperBenInvestigationANC(); + wrapper.setBeneficiaryRegID(1L); + wrapper.setBenVisitID(2L); + wrapper.setVisitCode(3L); + wrapper.setPrescriptionID(4L); + com.iemr.mmu.data.quickConsultation.LabTestOrderDetail order = + new com.iemr.mmu.data.quickConsultation.LabTestOrderDetail(); + ArrayList orders = new ArrayList<>( + Collections.singletonList(order)); + wrapper.setLaboratoryList(orders); + when(labTestOrderDetailRepo.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + assertEquals(1L, service.saveBenInvestigation(wrapper)); + assertEquals(4L, order.getPrescriptionID()); + assertEquals(1L, order.getBeneficiaryRegID()); + assertEquals(3L, order.getVisitCode()); + } + + @Test + void saveBenInvestigation_succeedsWhenNoTestWasOrdered() { + com.iemr.mmu.data.anc.WrapperBenInvestigationANC wrapper = + new com.iemr.mmu.data.anc.WrapperBenInvestigationANC(); + assertEquals(1L, service.saveBenInvestigation(wrapper)); + } + + @Test + void updateBenVisitStatusFlag_reportsSuccessWhenTheFlagWasStored() { + when(benVisitDetailRepo.updateBenFlowStatus("N", 1L)).thenReturn(1); + assertTrue(service.updateBenVisitStatusFlag(1L, "N").contains("Updated Successfully")); + } + + @Test + void updateBenStatus_returnsAnEmptyResultWhenNothingWasUpdated() { + when(benVisitDetailRepo.updateBenFlowStatus("N", 1L)).thenReturn(0); + assertEquals("{}", service.updateBenStatus(1L, "N")); + } + + @Test + void getNurseWorkList_serialisesTheRegistrarWorklist() { + when(reistrarRepoBenSearch.getNurseWorkList()).thenReturn(new ArrayList<>()); + assertNotNull(service.getNurseWorkList()); + } + + @Test + void theRoleWorklistsFallBackToASevenDayWindowWhenNoLimitIsConfigured() { + when(beneficiaryFlowStatusRepo.getNurseWorklistNew(any(), any(), any())).thenReturn(new ArrayList<>()); + when(beneficiaryFlowStatusRepo.getLabWorklistNew(any(), any(), any())).thenReturn(new ArrayList<>()); + when(beneficiaryFlowStatusRepo.getRadiologistWorkListNew(any(), any(), any())) + .thenReturn(new ArrayList<>()); + when(beneficiaryFlowStatusRepo.getOncologistWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>()); + when(beneficiaryFlowStatusRepo.getPharmaWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>()); + + assertEquals("[]", service.getNurseWorkListNew(1, 2)); + assertEquals("[]", service.getLabWorkListNew(1, 2)); + assertEquals("[]", service.getRadiologistWorkListNew(1, 2)); + assertEquals("[]", service.getOncologistWorkListNew(1, 2)); + assertEquals("[]", service.getPharmaWorkListNew(1, 2)); + } + + @Test + void theRoleWorklistsHonourTheConfiguredDayWindow() { + org.springframework.test.util.ReflectionTestUtils.setField(service, "nurseWL", 10); + org.springframework.test.util.ReflectionTestUtils.setField(service, "labWL", 10); + org.springframework.test.util.ReflectionTestUtils.setField(service, "radioWL", 10); + org.springframework.test.util.ReflectionTestUtils.setField(service, "oncoWL", 10); + org.springframework.test.util.ReflectionTestUtils.setField(service, "pharmaWL", 10); + org.springframework.test.util.ReflectionTestUtils.setField(service, "TMReferredWL", 10); + + when(beneficiaryFlowStatusRepo.getNurseWorklistNew(any(), any(), any())).thenReturn(new ArrayList<>()); + when(beneficiaryFlowStatusRepo.getNurseWorklistTMreferred(any(), any(), any())) + .thenReturn(new ArrayList<>()); + when(beneficiaryFlowStatusRepo.getLabWorklistNew(any(), any(), any())).thenReturn(new ArrayList<>()); + when(beneficiaryFlowStatusRepo.getRadiologistWorkListNew(any(), any(), any())) + .thenReturn(new ArrayList<>()); + when(beneficiaryFlowStatusRepo.getOncologistWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>()); + when(beneficiaryFlowStatusRepo.getPharmaWorkListNew(any(), any(), any())).thenReturn(new ArrayList<>()); + + assertEquals("[]", service.getNurseWorkListNew(1, 2)); + assertEquals("[]", service.getNurseWorkListTMReferred(1, 2)); + assertEquals("[]", service.getLabWorkListNew(1, 2)); + assertEquals("[]", service.getRadiologistWorkListNew(1, 2)); + assertEquals("[]", service.getOncologistWorkListNew(1, 2)); + assertEquals("[]", service.getPharmaWorkListNew(1, 2)); + } + } + + @Nested + @DisplayName("child history and screening") + class ChildHistoryAndScreening { + + @Test + void saveBenAdherenceDetails_reportsWhetherTheRowWasStored() { + com.iemr.mmu.data.anc.BenAdherence adherence = new com.iemr.mmu.data.anc.BenAdherence(); + when(benAdherenceRepo.save(adherence)).thenReturn(adherence); + assertEquals(1, service.saveBenAdherenceDetails(adherence)); + + when(benAdherenceRepo.save(adherence)).thenReturn(null); + assertEquals(0, service.saveBenAdherenceDetails(adherence)); + } + + @Test + void saveChildDevelopmentHistory_returnsTheStoredId() { + com.iemr.mmu.data.anc.BenChildDevelopmentHistory history = + new com.iemr.mmu.data.anc.BenChildDevelopmentHistory(); + com.iemr.mmu.data.anc.BenChildDevelopmentHistory stored = + new com.iemr.mmu.data.anc.BenChildDevelopmentHistory(); + stored.setID(31L); + when(benChildDevelopmentHistoryRepo.save(any())).thenReturn(stored); + assertEquals(31L, service.saveChildDevelopmentHistory(history)); + + stored.setID(0L); + assertNull(service.saveChildDevelopmentHistory(history)); + } + + @Test + void saveChildFeedingHistory_returnsTheStoredId() { + com.iemr.mmu.data.anc.ChildFeedingDetails details = new com.iemr.mmu.data.anc.ChildFeedingDetails(); + com.iemr.mmu.data.anc.ChildFeedingDetails stored = new com.iemr.mmu.data.anc.ChildFeedingDetails(); + stored.setID(32L); + when(childFeedingDetailsRepo.save(details)).thenReturn(stored); + assertEquals(32L, service.saveChildFeedingHistory(details)); + + stored.setID(0L); + assertNull(service.saveChildFeedingHistory(details)); + } + + @Test + void savePerinatalHistory_returnsTheStoredId() { + com.iemr.mmu.data.anc.PerinatalHistory history = new com.iemr.mmu.data.anc.PerinatalHistory(); + com.iemr.mmu.data.anc.PerinatalHistory stored = new com.iemr.mmu.data.anc.PerinatalHistory(); + stored.setID(33L); + when(perinatalHistoryRepo.save(history)).thenReturn(stored); + assertEquals(33L, service.savePerinatalHistory(history)); + + stored.setID(0L); + assertNull(service.savePerinatalHistory(history)); + } + + @Test + void getBenAdherence_andGetLabTestOrders_serialiseTheStoredRows() { + when(benAdherenceRepo.getBenAdherence(1L, 2L)).thenReturn(new ArrayList<>()); + assertNotNull(service.getBenAdherence(1L, 2L)); + + when(labTestOrderDetailRepo.getLabTestOrderDetails(1L, 2L)).thenReturn(new ArrayList<>()); + assertNotNull(service.getLabTestOrders(1L, 2L)); + } + + @Test + void theChildHistoryReadsMapTheStoredRows() { + when(benChildDevelopmentHistoryRepo.getBenDevelopmentDetails(1L, 2L)).thenReturn(new ArrayList<>()); + service.getDevelopmentHistory(1L, 2L); + + when(perinatalHistoryRepo.getBenPerinatalDetails(1L, 2L)).thenReturn(new ArrayList<>()); + service.getPerinatalHistory(1L, 2L); + + when(childFeedingDetailsRepo.getBenFeedingDetails(1L, 2L)).thenReturn(new ArrayList<>()); + service.getFeedingHistory(1L, 2L); + } + + @Test + void updateChildFeedingHistory_updatesTheStoredRowWhenOneExists() { + com.iemr.mmu.data.anc.ChildFeedingDetails details = new com.iemr.mmu.data.anc.ChildFeedingDetails(); + when(childFeedingDetailsRepo.getBenChildFeedingDetailStatus(any(), any())).thenReturn("P"); + when(childFeedingDetailsRepo.updateFeedingDetails(any(), any(), any(), any(), any(), any(), any(), any(), + org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1); + + assertEquals(1, service.updateChildFeedingHistory(details)); + } + + @Test + void updateChildFeedingHistory_insertsAFreshRowWhenNoneIsStoredYet() { + com.iemr.mmu.data.anc.ChildFeedingDetails details = new com.iemr.mmu.data.anc.ChildFeedingDetails(); + details.setModifiedBy("nurse"); + com.iemr.mmu.data.anc.ChildFeedingDetails stored = new com.iemr.mmu.data.anc.ChildFeedingDetails(); + stored.setID(34L); + when(childFeedingDetailsRepo.getBenChildFeedingDetailStatus(any(), any())).thenReturn(null); + when(childFeedingDetailsRepo.save(details)).thenReturn(stored); + + assertEquals(1, service.updateChildFeedingHistory(details)); + assertEquals("nurse", details.getCreatedBy()); + assertEquals(0, service.updateChildFeedingHistory(null)); + } + + @Test + void updatePerinatalHistory_updatesTheStoredRowWhenOneExists() { + com.iemr.mmu.data.anc.PerinatalHistory history = new com.iemr.mmu.data.anc.PerinatalHistory(); + when(perinatalHistoryRepo.getPerinatalHistoryStatus(any(), any())).thenReturn("P"); + when(perinatalHistoryRepo.updatePerinatalDetails(any(), any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1); + + assertEquals(1, service.updatePerinatalHistory(history)); + } + + @Test + void updatePerinatalHistory_insertsAFreshRowWhenNoneIsStoredYet() { + com.iemr.mmu.data.anc.PerinatalHistory history = new com.iemr.mmu.data.anc.PerinatalHistory(); + com.iemr.mmu.data.anc.PerinatalHistory stored = new com.iemr.mmu.data.anc.PerinatalHistory(); + stored.setID(35L); + when(perinatalHistoryRepo.getPerinatalHistoryStatus(any(), any())).thenReturn(null); + when(perinatalHistoryRepo.save(history)).thenReturn(stored); + + assertEquals(1, service.updatePerinatalHistory(history)); + assertEquals(0, service.updatePerinatalHistory(null)); + } + + @Test + void updateChildDevelopmentHistory_updatesTheStoredRowWhenOneExists() { + com.iemr.mmu.data.anc.BenChildDevelopmentHistory history = + new com.iemr.mmu.data.anc.BenChildDevelopmentHistory(); + when(benChildDevelopmentHistoryRepo.getDevelopmentHistoryStatus(any(), any())).thenReturn("P"); + when(benChildDevelopmentHistoryRepo.updatePerinatalDetails(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), org.mockito.ArgumentMatchers.eq("U"), any(), any())).thenReturn(1); + + assertEquals(1, service.updateChildDevelopmentHistory(history)); + } + + @Test + void updateChildDevelopmentHistory_insertsAFreshRowWhenNoneIsStoredYet() { + com.iemr.mmu.data.anc.BenChildDevelopmentHistory history = + new com.iemr.mmu.data.anc.BenChildDevelopmentHistory(); + com.iemr.mmu.data.anc.BenChildDevelopmentHistory stored = + new com.iemr.mmu.data.anc.BenChildDevelopmentHistory(); + stored.setID(36L); + when(benChildDevelopmentHistoryRepo.getDevelopmentHistoryStatus(any(), any())).thenReturn(null); + when(benChildDevelopmentHistoryRepo.save(any())).thenReturn(stored); + + assertEquals(1, service.updateChildDevelopmentHistory(history)); + assertEquals(0, service.updateChildDevelopmentHistory(null)); + } + + @Test + void updateBenFamilyHistoryNCDScreening_reportsWhetherEveryDiseaseWasStored() { + BenFamilyHistory input = mock(BenFamilyHistory.class); + ArrayList entries = new ArrayList<>(Collections.singletonList(new BenFamilyHistory())); + when(input.getBenFamilyHist()).thenReturn(entries); + when(benFamilyHistoryRepo.saveAll(entries)).thenReturn(entries); + assertEquals(1, service.updateBenFamilyHistoryNCDScreening(input)); + + when(input.getBenFamilyHist()).thenReturn(new ArrayList<>()); + assertEquals(0, service.updateBenFamilyHistoryNCDScreening(input)); + } + + @Test + void updateBenPhysicalActivityHistoryNCDScreening_marksAnExistingRowAsUpdated() { + PhysicalActivityType activity = new PhysicalActivityType(); + activity.setID(1L); + when(physicalActivityTypeRepo.save(activity)).thenReturn(activity); + + assertEquals(1, service.updateBenPhysicalActivityHistoryNCDScreening(activity)); + assertEquals("U", activity.getProcessed()); + assertEquals(Boolean.FALSE, activity.getDeleted()); + } + + @Test + void updateBenPhysicalActivityHistoryNCDScreening_marksAFreshRowAsNew() { + PhysicalActivityType activity = new PhysicalActivityType(); + when(physicalActivityTypeRepo.save(activity)).thenReturn(null); + + assertEquals(0, service.updateBenPhysicalActivityHistoryNCDScreening(activity)); + assertEquals("N", activity.getProcessed()); + } + } + + @Nested + @DisplayName("graph trends and NCD summaries") + class TrendsAndSummaries { + + @Test + void getGraphicalTrendData_readsWeightAndVitalsFromTheNonCancerVisits() { + ArrayList visits = new ArrayList<>(); + visits.add(new Object[] { 1L, "ANC", 100L }); + visits.add(new Object[] { 2L, "ANC", null }); + when(benVisitDetailRepo.getLastSixVisitDetailsForBeneficiary(1L)).thenReturn(visits); + + ArrayList anthro = new ArrayList<>(); + anthro.add(new Object[] { 55.5d, Date.valueOf("2024-01-01") }); + when(benAnthropometryRepo.getBenAnthropometryDetailForGraphtrends(any())).thenReturn(anthro); + + ArrayList vitals = new ArrayList<>(); + vitals.add(new Object[] { (short) 120, (short) 80, 90d, 140d, 150d, Date.valueOf("2024-01-01") }); + vitals.add(new Object[] { (short) 0, (short) 0, null, null, null, Date.valueOf("2024-01-02") }); + when(benPhysicalVitalRepo.getBenPhysicalVitalDetailForGraphTrends(any())).thenReturn(vitals); + + Map trends = service.getGraphicalTrendData(1L, "ANC"); + + assertEquals(1, ((List) trends.get("weightList")).size()); + assertEquals(1, ((List) trends.get("bpList")).size()); + assertEquals(1, ((List) trends.get("bgList")).size()); + } + + @Test + void getGraphicalTrendData_averagesTheThreeReadingsTakenAtCancerScreeningVisits() { + ArrayList visits = new ArrayList<>(); + visits.add(new Object[] { 1L, "Cancer Screening", 100L }); + when(benVisitDetailRepo.getLastSixVisitDetailsForBeneficiary(1L)).thenReturn(visits); + + com.iemr.mmu.data.nurse.BenCancerVitalDetail cancerVital = + new com.iemr.mmu.data.nurse.BenCancerVitalDetail(); + cancerVital.setWeight_Kg(60d); + cancerVital.setCreatedDate(new java.sql.Timestamp(System.currentTimeMillis())); + cancerVital.setSystolicBP_1stReading((short) 120); + cancerVital.setSystolicBP_2ndReading((short) 130); + cancerVital.setSystolicBP_3rdReading((short) 140); + cancerVital.setDiastolicBP_1stReading((short) 80); + cancerVital.setDiastolicBP_2ndReading((short) 90); + cancerVital.setDiastolicBP_3rdReading((short) 100); + cancerVital.setBloodGlucose_Fasting((short) 95); + + ArrayList cancerVitals = new ArrayList<>( + Collections.singletonList(cancerVital)); + when(benCancerVitalDetailRepo.getBenCancerVitalDetailForGraph(any())).thenReturn(cancerVitals); + + Map trends = service.getGraphicalTrendData(1L, "Cancer Screening"); + + assertEquals(1, ((List) trends.get("weightList")).size()); + List bpList = (List) trends.get("bpList"); + assertEquals(1, bpList.size()); + assertEquals(130, ((Map) bpList.get(0)).get("avgSysBP")); + assertEquals(90, ((Map) bpList.get(0)).get("avgDysBP")); + assertEquals(1, ((List) trends.get("bgList")).size()); + } + + @Test + void getGraphicalTrendData_returnsEmptyTrendsWhenTheBeneficiaryHasNoVisits() { + when(benVisitDetailRepo.getLastSixVisitDetailsForBeneficiary(1L)).thenReturn(new ArrayList<>()); + Map trends = service.getGraphicalTrendData(1L, "ANC"); + assertTrue(((List) trends.get("weightList")).isEmpty()); + } + + @Test + void getBenSymptomaticData_collectsTheAnswersOfTheMostRecentVisit() throws Exception { + IDRSData first = new IDRSData(); + first.setVisitCode(1L); + first.setIdrsQuestionID(10); + first.setAnswer("Yes"); + first.setSuspectedDisease("Diabetes"); + first.setConfirmedDisease("None"); + IDRSData sameVisit = new IDRSData(); + sameVisit.setVisitCode(1L); + sameVisit.setIdrsQuestionID(11); + IDRSData earlierVisit = new IDRSData(); + earlierVisit.setVisitCode(2L); + + when(iDRSDataRepo.getBenIdrsDetailsLast_3_Month(anyLong(), any())) + .thenReturn(new ArrayList<>(Arrays.asList(first, sameVisit, earlierVisit))); + when(iDRSDataRepo.isDiabeticCheck(1L)).thenReturn(1); + when(iDRSDataRepo.isHypertensionCheck(1L)).thenReturn(1); + when(iDRSDataRepo.isEpilepsyCheck(1L)).thenReturn(1); + when(iDRSDataRepo.isDefectiveVisionCheck(1L)).thenReturn(1); + + String json = service.getBenSymptomaticData(1L); + + assertTrue(json.contains("\"isDiabetic\":true"), json); + assertTrue(json.contains("\"isHypertension\":true"), json); + assertTrue(json.contains("\"isEpilepsy\":true"), json); + assertTrue(json.contains("\"isDefectiveVision\":true"), json); + assertTrue(json.contains("\"suspectedDisease\":\"Diabetes\""), json); + assertTrue(json.contains("\"confirmedDisease\":\"None\""), json); + } + + @Test + void getBenSymptomaticData_reportsNoConditionsWhenNothingWasScreened() throws Exception { + when(iDRSDataRepo.getBenIdrsDetailsLast_3_Month(anyLong(), any())).thenReturn(new ArrayList<>()); + when(iDRSDataRepo.isDiabeticCheck(1L)).thenReturn(0); + when(iDRSDataRepo.isHypertensionCheck(1L)).thenReturn(0); + when(iDRSDataRepo.isEpilepsyCheck(1L)).thenReturn(null); + when(iDRSDataRepo.isDefectiveVisionCheck(1L)).thenReturn(null); + + String json = service.getBenSymptomaticData(1L); + + assertTrue(json.contains("\"isDiabetic\":false"), json); + assertTrue(json.contains("\"isEpilepsy\":false"), json); + assertTrue(json.contains("\"questionariesData\":[]"), json); + } + + @Test + void getBenPreviousDiabetesData_keepsOnlyTheDiabetesQuestionsOfEachRow() throws Exception { + IDRSData diabetesRow = new IDRSData(); + diabetesRow.setDiseaseQuestionType("Diabetes||Hypertension"); + diabetesRow.setAnswer("Yes||No"); + diabetesRow.setQuestion("Do you have diabetes?||Do you have hypertension?"); + diabetesRow.setQuestionIds("1||2"); + IDRSData otherRow = new IDRSData(); + otherRow.setDiseaseQuestionType("Hypertension"); + otherRow.setAnswer("No"); + otherRow.setQuestion("Do you have hypertension?"); + otherRow.setQuestionIds("2"); + + when(iDRSDataRepo.getBenPreviousDiabetesDetails(1L)) + .thenReturn(new ArrayList<>(Arrays.asList(diabetesRow, otherRow))); + + String json = service.getBenPreviousDiabetesData(1L); + + assertTrue(json.contains("Do you have diabetes?"), json); + assertTrue(json.contains("\"columns\""), json); + } + + @Test + void getBenPreviousReferralData_mapsEachStoredReferral() throws Exception { + ArrayList referrals = new ArrayList<>(); + referrals.add(new Object[] { java.math.BigInteger.valueOf(5), + new java.sql.Timestamp(System.currentTimeMillis()), "Diabetes" }); + when(iDRSDataRepo.getBenPreviousReferredDetails(1L)).thenReturn(referrals); + + String json = service.getBenPreviousReferralData(1L); + assertTrue(json.contains("Diabetes"), json); + + when(iDRSDataRepo.getBenPreviousReferredDetails(2L)).thenReturn(null); + assertTrue(service.getBenPreviousReferralData(2L).contains("\"data\":[]")); + } + + @org.junit.jupiter.params.ParameterizedTest(name = "a BMI of {0} reads as {1}") + @org.junit.jupiter.params.provider.CsvSource({ "0.5, Normal", "-1.5, Mild malnourished", + "-2.5, Moderately Malnourished", "-4.0, Severely Malnourished", "1.5, Overweight", "2.5, Obese", + "3.5, Severely Obese" }) + void calculateBMIStatus_classifiesTheBmiAgainstTheStoredStandardDeviations(double bmi, String expectedStatus) + throws Exception { + com.iemr.mmu.data.bmi.BmiCalculation standard = new com.iemr.mmu.data.bmi.BmiCalculation(); + standard.setN3SD(-3d); + standard.setN2SD(-2d); + standard.setN1SD(-1d); + standard.setP1SD(1d); + standard.setP2SD(2d); + standard.setP3SD(3d); + when(bmiCalculationRepo.getBMIDetails(27, "Male")).thenReturn(standard); + + String request = "{\"yearMonth\":\"2 Years and 3 Months\",\"gender\":\"Male\",\"bmi\":" + bmi + "}"; + assertTrue(service.calculateBMIStatus(request).contains(expectedStatus)); + } + + @Test + void calculateBMIStatus_reportsNoStatusWhenTheRequestIsIncomplete() throws Exception { + assertEquals("{\"bmiStatus\":\"\"}", service.calculateBMIStatus("{}")); + } + + @Test + void calculateBMIStatus_failsWhenNoStandardIsStoredForTheCategory() { + when(bmiCalculationRepo.getBMIDetails(anyInt(), anyString())).thenReturn(null); + String request = "{\"yearMonth\":\"2 Years and 3 Months\",\"gender\":\"Male\",\"bmi\":1.0}"; + IEMRException thrown = assertThrows(IEMRException.class, () -> service.calculateBMIStatus(request)); + assertTrue(thrown.getMessage().contains("No data found for this category")); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/common/transaction/CommonServiceImplTest.java b/src/test/java/com/iemr/mmu/service/common/transaction/CommonServiceImplTest.java new file mode 100644 index 00000000..c571661c --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/common/transaction/CommonServiceImplTest.java @@ -0,0 +1,560 @@ +/* +* 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.mmu.service.common.transaction; + +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.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +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.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus; +import com.iemr.mmu.data.syncActivity_syncLayer.EmployeeSignature; +import com.iemr.mmu.data.syncActivity_syncLayer.DownloadedCaseSheet; +import com.iemr.mmu.data.common.DocFileManager; +import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo; +import com.iemr.mmu.repo.syncActivity_syncLayer.EmployeeSignatureRepo; +import com.iemr.mmu.repo.nurse.ncdscreening.IDRSDataRepo; +import com.iemr.mmu.repo.provider.ProviderServiceMappingRepo; +import com.iemr.mmu.repo.syncActivity_syncLayer.DownloadedCaseSheetRepo; +import com.iemr.mmu.service.anc.ANCServiceImpl; +import com.iemr.mmu.service.cancerScreening.CSNurseServiceImpl; +import com.iemr.mmu.service.cancerScreening.CSServiceImpl; +import com.iemr.mmu.service.covid19.Covid19ServiceImpl; +import com.iemr.mmu.service.generalOPD.GeneralOPDServiceImpl; +import com.iemr.mmu.service.ncdCare.NCDCareServiceImpl; +import com.iemr.mmu.service.ncdscreening.NCDScreeningServiceImpl; +import com.iemr.mmu.service.pnc.PNCServiceImpl; +import com.iemr.mmu.service.quickConsultation.QuickConsultationServiceImpl; +import com.iemr.mmu.utils.AESEncryption.AESEncryptionDecryption; +import com.iemr.mmu.utils.CookieUtil; +import com.iemr.mmu.utils.exception.IEMRException; + +class CommonServiceImplTest { + + @Mock + private Covid19ServiceImpl covid19ServiceImpl; + @Mock + private AESEncryptionDecryption aESEncryptionDecryption; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + @Mock + private ANCServiceImpl ancServiceImpl; + @Mock + private PNCServiceImpl pncServiceImpl; + @Mock + private GeneralOPDServiceImpl generalOPDServiceImpl; + @Mock + private NCDCareServiceImpl ncdCareServiceImpl; + @Mock + private QuickConsultationServiceImpl quickConsultationServiceImpl; + @Mock + private CommonNurseServiceImpl commonNurseServiceImpl; + @Mock + private CSNurseServiceImpl cSNurseServiceImpl; + @Mock + private CSServiceImpl csServiceImpl; + @Mock + private NCDScreeningServiceImpl ncdScreeningServiceImpl; + @Mock + private ProviderServiceMappingRepo providerServiceMappingRepo; + @Mock + private DownloadedCaseSheetRepo downloadedCaseSheetRepo; + @Mock + private IDRSDataRepo iDRSDataRepo; + @Mock + private EmployeeSignatureRepo employeeSignatureRepo; + @Mock + private CookieUtil cookieUtil; + + @InjectMocks + private CommonServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(service, "mmuCentralServer", "http://central/casesheet"); + ReflectionTestUtils.setField(service, "tmCentralServer", "http://central/tm"); + ReflectionTestUtils.setField(service, "specialistSign", "http://central/sign"); + } + + @AfterEach + void clearRequestContext() { + RequestContextHolder.resetRequestAttributes(); + } + + private static BeneficiaryFlowStatus flow(String visitCategory) { + BeneficiaryFlowStatus flow = new BeneficiaryFlowStatus(); + flow.setVisitCategory(visitCategory); + flow.setBeneficiaryRegID(1L); + flow.setBenVisitCode(2L); + flow.setVisitCode(2L); + flow.setBenFlowID(3L); + flow.setBenVisitID(4L); + return flow; + } + + @Nested + @DisplayName("case sheet print data") + class PrintData { + + @BeforeEach + void stubTheLeftPanel() { + when(beneficiaryFlowStatusRepo.getBenDetailsForLeftSidePanel(1L, 3L)).thenReturn(new ArrayList<>()); + } + + @Test + void getCaseSheetPrintDataForBeneficiary_routesEachVisitCategoryToItsOwnPrintData() throws Exception { + assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("ANC"), "auth").contains("nurseData")); + assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("PNC"), "auth").contains("nurseData")); + assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("General OPD"), "auth") + .contains("nurseData")); + assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("NCD care"), "auth").contains("nurseData")); + assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("General OPD (QC)"), "auth") + .contains("nurseData")); + assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("COVID-19 Screening"), "auth") + .contains("nurseData")); + assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("NCD screening"), "auth") + .contains("nurseData")); + } + + @Test + void getCaseSheetPrintDataForBeneficiary_addsTheAnnotatedImagesForCancerScreening() throws Exception { + when(cSNurseServiceImpl.getCancerExaminationImageAnnotationCasesheet(1L, 2L)) + .thenReturn(new ArrayList<>()); + + assertTrue(service.getCaseSheetPrintDataForBeneficiary(flow("Cancer Screening"), "auth") + .contains("ImageAnnotatedData")); + } + + @Test + void getCaseSheetPrintDataForBeneficiary_rejectsAnUnknownVisitCategory() throws Exception { + assertEquals("Invalid VisitCategory", + service.getCaseSheetPrintDataForBeneficiary(flow("Unknown"), "auth")); + } + } + + @Nested + @DisplayName("past history reads") + class PastHistoryReads { + + @Test + void everyPastHistoryReadDelegatesToTheNurseService() throws Exception { + when(commonNurseServiceImpl.fetchBenPastMedicalHistory(1L)).thenReturn("past"); + assertEquals("past", service.getBenPastHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenComorbidityHistory(1L)).thenReturn("comorbid"); + assertEquals("comorbid", service.getComorbidHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenPersonalMedicationHistory(1L)).thenReturn("medication"); + assertEquals("medication", service.getMedicationHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenPersonalTobaccoHistory(1L)).thenReturn("tobacco"); + assertEquals("tobacco", service.getPersonalTobaccoHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenPersonalAlcoholHistory(1L)).thenReturn("alcohol"); + assertEquals("alcohol", service.getPersonalAlcoholHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenPersonalAllergyHistory(1L)).thenReturn("allergy"); + assertEquals("allergy", service.getPersonalAllergyHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenPersonalFamilyHistory(1L)).thenReturn("family"); + assertEquals("family", service.getFamilyHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenPhysicalHistory(1L)).thenReturn("physical"); + assertEquals("physical", service.getBenPhysicalHistory(1L)); + + when(commonNurseServiceImpl.fetchBenMenstrualHistory(1L)).thenReturn("menstrual"); + assertEquals("menstrual", service.getMenstrualHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenPastObstetricHistory(1L)).thenReturn("obstetric"); + assertEquals("obstetric", service.getObstetricHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenImmunizationHistory(1L)).thenReturn("immunization"); + assertEquals("immunization", service.getImmunizationHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenOptionalVaccineHistory(1L)).thenReturn("vaccine"); + assertEquals("vaccine", service.getChildVaccineHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenPerinatalHistory(1L)).thenReturn("perinatal"); + assertEquals("perinatal", service.getBenPerinatalHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenFeedingHistory(1L)).thenReturn("feeding"); + assertEquals("feeding", service.getBenFeedingHistoryData(1L)); + + when(commonNurseServiceImpl.fetchBenDevelopmentHistory(1L)).thenReturn("development"); + assertEquals("development", service.getBenDevelopmentHistoryData(1L)); + + when(commonNurseServiceImpl.getBenSymptomaticData(1L)).thenReturn("symptomatic"); + assertEquals("symptomatic", service.getBenSymptomaticQuestionnaireDetailsData(1L)); + + when(commonNurseServiceImpl.getBenPreviousDiabetesData(1L)).thenReturn("diabetes"); + assertEquals("diabetes", service.getBenPreviousDiabetesData(1L)); + + when(commonNurseServiceImpl.getBenPreviousReferralData(1L)).thenReturn("referral"); + assertEquals("referral", service.getBenPreviousReferralData(1L)); + } + + @Test + void getBenPreviousVisitDataForCaseRecord_looksTheVisitsUpAgainstEveryMmuProvider() throws Exception { + when(providerServiceMappingRepo.getProviderServiceMapIdForServiceID((short) 2)) + .thenReturn(new ArrayList<>(Collections.singletonList(5))); + when(beneficiaryFlowStatusRepo.getBenPreviousHistory(eq(1L), any())).thenReturn(new ArrayList<>()); + + assertNotNull(service.getBenPreviousVisitDataForCaseRecord("{\"beneficiaryRegID\":1}")); + } + } + + @Nested + @DisplayName("uploaded files") + class Files { + + @TempDir + Path uploadRoot; + + @Test + void saveFiles_writesEachAttachmentUnderTheVanAndDateFolderAndEncryptsItsPath() throws Exception { + ReflectionTestUtils.setField(service, "fileBasePath", uploadRoot.toString() + "/"); + when(aESEncryptionDecryption.encrypt(anyString())).thenReturn("encrypted"); + + DocFileManager attachment = new DocFileManager(); + attachment.setVanID(7); + attachment.setFileName("re*port.pdf"); + attachment.setFileExtension(".pdf"); + attachment.setFileContent(Base64.getEncoder().encodeToString("content".getBytes())); + + String response = service.saveFiles(Collections.singletonList(attachment)); + + assertTrue(response.contains("encrypted")); + assertTrue(response.contains("report.pdf")); + } + + @Test + void saveFiles_reusesTheDateFolderOnASecondUpload() throws Exception { + ReflectionTestUtils.setField(service, "fileBasePath", uploadRoot.toString() + "/"); + when(aESEncryptionDecryption.encrypt(anyString())).thenReturn("encrypted"); + + DocFileManager attachment = new DocFileManager(); + attachment.setVanID(7); + attachment.setFileName("report.pdf"); + attachment.setFileExtension(".pdf"); + attachment.setFileContent(Base64.getEncoder().encodeToString("content".getBytes())); + + service.saveFiles(Collections.singletonList(attachment)); + assertTrue(service.saveFiles(Collections.singletonList(attachment)).contains("encrypted")); + } + + @Test + void saveFiles_skipsAnAttachmentWithoutANameOrExtension() throws Exception { + ReflectionTestUtils.setField(service, "fileBasePath", uploadRoot.toString() + "/"); + + DocFileManager attachment = new DocFileManager(); + attachment.setVanID(7); + + assertEquals("[]", service.saveFiles(Collections.singletonList(attachment))); + } + + @Test + void saveFiles_returnsNothingWhenNoAttachmentWasSent() throws Exception { + ReflectionTestUtils.setField(service, "fileBasePath", uploadRoot.toString() + "/"); + assertEquals("[]", service.saveFiles(new ArrayList<>())); + } + + @Test + void loadFileAsResource_returnsAnExistingFile() throws Exception { + Path file = uploadRoot.resolve("report.pdf"); + java.nio.file.Files.write(file, "content".getBytes()); + + assertNotNull(service.loadFileAsResource("report.pdf", file.toString())); + } + + @Test + void loadFileAsResource_failsForAMissingFile() { + assertThrows(IOException.class, + () -> service.loadFileAsResource("gone.pdf", uploadRoot.resolve("gone.pdf").toString())); + } + } + + @Nested + @DisplayName("teleconsultation case sheets") + class TeleconsultationCaseSheets { + + private CommonServiceImpl serviceSpy; + + @BeforeEach + void useASpyForTheOutboundCalls() { + serviceSpy = spy(service); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest())); + } + + private ResponseEntity ok(String body) { + return new ResponseEntity<>(body, HttpStatus.OK); + } + + @Test + void checkIsCaseSheetDownloaded_reportsWhetherTheSheetWasAlreadyPulled() throws Exception { + when(beneficiaryFlowStatusRepo.checkIsCaseSheetDownloaded(1L)).thenReturn(true); + assertEquals(1, service.checkIsCaseSheetDownloaded(1L)); + + when(beneficiaryFlowStatusRepo.checkIsCaseSheetDownloaded(1L)).thenReturn(false); + assertEquals(0, service.checkIsCaseSheetDownloaded(1L)); + + when(beneficiaryFlowStatusRepo.checkIsCaseSheetDownloaded(1L)).thenReturn(null); + assertEquals(0, service.checkIsCaseSheetDownloaded(1L)); + } + + @Test + void getTmVisitCode_readsTheTeleconsultationVisitOfAnMmuVisit() throws Exception { + BeneficiaryFlowStatus tmVisit = new BeneficiaryFlowStatus(); + when(beneficiaryFlowStatusRepo.getTMVisitDetails(1L)).thenReturn(tmVisit); + assertEquals(tmVisit, service.getTmVisitCode(1L)); + } + + @Test + void getTmCaseSheet_returnsTheCaseSheetAndTheSpecialistSignature() throws Exception { + String caseSheet = "{\"statusCode\":200,\"data\":{\"BeneficiaryData\":{\"tCSpecialistUserID\":5}}}"; + String signature = "{\"statusCode\":200,\"data\":{\"userID\":5}}"; + doReturn(ok(caseSheet)).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), any()); + doReturn(ok(signature)).when(serviceSpy).restTemplateGet(anyString(), any(), any()); + + ArrayList result = serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth"); + + assertEquals(2, result.size()); + } + + @Test + void getTmCaseSheet_returnsOnlyTheCaseSheetWhenNoSpecialistSigned() throws Exception { + String caseSheet = "{\"statusCode\":200,\"data\":{\"BeneficiaryData\":{}}}"; + doReturn(ok(caseSheet)).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), any()); + + assertEquals(1, + serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth").size()); + } + + @Test + void getTmCaseSheet_reportsTheLoginFailureFromTheCentralServer() { + String error = "{\"statusCode\":5002,\"errorMessage\":\"Session expired\"}"; + doReturn(ok(error)).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), any()); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth")); + assertEquals(5002, thrown.getErrorCode()); + } + + @Test + void getTmCaseSheet_reportsAnyOtherFailureFromTheCentralServer() { + String error = "{\"statusCode\":5000,\"errorMessage\":\"Boom\"}"; + doReturn(ok(error)).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), any()); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth")); + assertEquals("Boom", thrown.getMessage()); + } + + @Test + void getTmCaseSheet_failsWhenTheCentralServerRejectsTheRequest() { + doReturn(new ResponseEntity(HttpStatus.BAD_REQUEST)).when(serviceSpy) + .restTemplatePost(anyString(), any(), anyString(), any()); + + assertThrows(IEMRException.class, + () -> serviceSpy.getTmCaseSheet(flow("NCD screening"), flow("NCD screening"), "auth")); + } + + @Test + void getTmCaseSheetOffline_returnsThePreviouslyDownloadedSheet() throws Exception { + DownloadedCaseSheet stored = new DownloadedCaseSheet(); + stored.setTmCaseSheetResponse("sheet"); + when(downloadedCaseSheetRepo.getTmCaseSheetFromOffline(2L)).thenReturn(stored); + + assertEquals("sheet", service.getTmCaseSheetOffline(flow("NCD screening"))); + } + + @Test + void getTmCaseSheetOffline_failsWhenNothingWasDownloadedYet() { + when(downloadedCaseSheetRepo.getTmCaseSheetFromOffline(2L)).thenReturn(null); + + assertThrows(IEMRException.class, () -> service.getTmCaseSheetOffline(flow("NCD screening"))); + } + + @Test + void getCaseSheetOfTm_returnsTheCaseSheetOnceTeleconsultationIsComplete() throws Exception { + BeneficiaryFlowStatus tmVisit = new BeneficiaryFlowStatus(); + tmVisit.setSpecialist_flag((short) 9); + when(beneficiaryFlowStatusRepo.getTMVisitDetails(any())).thenReturn(tmVisit); + doReturn(new ArrayList<>(Collections.singletonList("sheet"))).when(serviceSpy).getTmCaseSheet(any(), + any(), anyString()); + + assertTrue(serviceSpy.getCaseSheetOfTm("{\"benVisitCode\":2}", "auth").contains("sheet")); + } + + @Test + void getCaseSheetOfTm_failsWhileTeleconsultationIsStillPending() { + BeneficiaryFlowStatus tmVisit = new BeneficiaryFlowStatus(); + tmVisit.setSpecialist_flag((short) 1); + when(beneficiaryFlowStatusRepo.getTMVisitDetails(any())).thenReturn(tmVisit); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> service.getCaseSheetOfTm("{\"benVisitCode\":2}", "auth")); + assertEquals("Tele-Consultation is not completed", thrown.getMessage()); + } + + @Test + void getCaseSheetOfTm_failsWhileTheBeneficiaryIsStillInTheTeleconsultationWorklist() { + when(beneficiaryFlowStatusRepo.getTMVisitDetails(any())).thenReturn(null); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> service.getCaseSheetOfTm("{\"benVisitCode\":2}", "auth")); + assertEquals("Patient is waiting in Tele-Medicine worklist", thrown.getMessage()); + } + + /** The central-server payload for a downloaded case sheet, with an optional signature. */ + private String centralPayload(boolean withSignature) { + String caseSheet = "{\"nurseData\":{\"history\":{\"PhysicalActivityHistory\":{\"visitCode\":9," + + "\"createdBy\":\"specialist\"}},\"idrs\":{\"IDRSDetail\":{\"confirmedDisease\":\"Diabetes\"," + + "\"suspectedDisease\":\"Hypertension\"}}}}"; + String signature = withSignature ? ",{\"userID\":5}" : ""; + return "{\"statusCode\":200,\"data\":[" + caseSheet + signature + "]}"; + } + + @Test + void getCaseSheetFromCentralServer_storesTheSheetTheSignatureAndTheScreeningOutcome() throws Exception { + doReturn(ok(centralPayload(true))).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), + any()); + when(employeeSignatureRepo.findOneByUserID(5L)).thenReturn(null); + when(downloadedCaseSheetRepo.save(any())).thenReturn(new DownloadedCaseSheet()); + when(beneficiaryFlowStatusRepo.updateDownloadFlag(2L)).thenReturn(1); + when(iDRSDataRepo.updateConfirmedAndSuspectedDisease("Diabetes", "Hypertension", 2L)).thenReturn(1); + + assertTrue(serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth") + .contains("PhysicalActivityHistory")); + verify(employeeSignatureRepo).save(any()); + } + + @Test + void getCaseSheetFromCentralServer_keepsAnAlreadyStoredSpecialistSignature() throws Exception { + doReturn(ok(centralPayload(true))).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), + any()); + when(employeeSignatureRepo.findOneByUserID(5L)).thenReturn(new EmployeeSignature()); + when(downloadedCaseSheetRepo.save(any())).thenReturn(new DownloadedCaseSheet()); + when(beneficiaryFlowStatusRepo.updateDownloadFlag(2L)).thenReturn(1); + when(iDRSDataRepo.updateConfirmedAndSuspectedDisease(any(), any(), anyLong())).thenReturn(1); + + assertNotNull(serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth")); + verify(employeeSignatureRepo, org.mockito.Mockito.never()).save(any()); + } + + @Test + void getCaseSheetFromCentralServer_failsWhenTheScreeningOutcomeCouldNotBeStored() throws Exception { + doReturn(ok(centralPayload(false))).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), + any()); + when(downloadedCaseSheetRepo.save(any())).thenReturn(new DownloadedCaseSheet()); + when(beneficiaryFlowStatusRepo.updateDownloadFlag(2L)).thenReturn(1); + when(iDRSDataRepo.updateConfirmedAndSuspectedDisease(any(), any(), anyLong())).thenReturn(0); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth")); + assertTrue(thrown.getMessage().contains("confirmed and suspected disease")); + } + + @Test + void getCaseSheetFromCentralServer_failsWhenTheDownloadFlagCouldNotBeStored() throws Exception { + doReturn(ok(centralPayload(false))).when(serviceSpy).restTemplatePost(anyString(), any(), anyString(), + any()); + when(downloadedCaseSheetRepo.save(any())).thenReturn(new DownloadedCaseSheet()); + when(beneficiaryFlowStatusRepo.updateDownloadFlag(2L)).thenReturn(0); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth")); + assertTrue(thrown.getMessage().contains("download flag")); + } + + @Test + void getCaseSheetFromCentralServer_reportsTheLoginFailureFromTheCentralServer() { + doReturn(ok("{\"statusCode\":5002,\"errorMessage\":\"Session expired\"}")).when(serviceSpy) + .restTemplatePost(anyString(), any(), anyString(), any()); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth")); + assertEquals(5002, thrown.getErrorCode()); + } + + @Test + void getCaseSheetFromCentralServer_reportsAnyOtherFailureFromTheCentralServer() { + doReturn(ok("{\"statusCode\":5000,\"errorMessage\":\"Boom\"}")).when(serviceSpy) + .restTemplatePost(anyString(), any(), anyString(), any()); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth")); + assertEquals("Boom", thrown.getMessage()); + } + + @Test + void getCaseSheetFromCentralServer_failsWhenTheCentralServerRejectsTheRequest() { + doReturn(new ResponseEntity(HttpStatus.BAD_REQUEST)).when(serviceSpy) + .restTemplatePost(anyString(), any(), anyString(), any()); + + assertThrows(IEMRException.class, + () -> serviceSpy.getCaseSheetFromCentralServer("{\"visitCode\":2}", "auth")); + } + + @Test + void updateConfirmedDisease_delegatesToTheScreeningRepository() { + when(iDRSDataRepo.updateConfirmedAndSuspectedDisease("Diabetes", "Hypertension", 2L)).thenReturn(1); + assertEquals(1, service.updateConfirmedDisease("Diabetes", "Hypertension", 2L)); + } + + @Test + void getJsonObj_parsesTheResponseBody() { + assertTrue(service.getJsonObj(ok("{\"statusCode\":200}")).has("statusCode")); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/covid19/Covid19ServiceImplTest.java b/src/test/java/com/iemr/mmu/service/covid19/Covid19ServiceImplTest.java new file mode 100644 index 00000000..e9ad2627 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/covid19/Covid19ServiceImplTest.java @@ -0,0 +1,575 @@ +/* +* 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.mmu.service.covid19; + +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.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; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +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.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus; +import com.iemr.mmu.data.covid19.Covid19BenFeedback; +import com.iemr.mmu.data.nurse.CommonUtilityClass; +import com.iemr.mmu.data.quickConsultation.PrescriptionDetail; +import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo; +import com.iemr.mmu.repo.nurse.covid19.Covid19BenFeedbackRepo; +import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo; +import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonServiceImpl; +import com.iemr.mmu.service.labtechnician.LabTechnicianServiceImpl; + +class Covid19ServiceImplTest { + + @Mock + private CommonNurseServiceImpl commonNurseServiceImpl; + @Mock + private CommonDoctorServiceImpl commonDoctorServiceImpl; + @Mock + private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl; + @Mock + private LabTechnicianServiceImpl labTechnicianServiceImpl; + @Mock + private CommonServiceImpl commonServiceImpl; + @Mock + private Covid19BenFeedbackRepo covid19BenFeedbackRepo; + @Mock + private PrescriptionDetailRepo prescriptionDetailRepo; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + + @InjectMocks + private Covid19ServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + private static String visitDetailsBlock(String covidDetails) { + return "\"visitDetails\":{\"visitDetails\":{\"beneficiaryRegID\":1,\"visitReason\":\"New Chief Complaint\"," + + "\"visitCategory\":\"COVID-19 Screening\"},\"covidDetails\":" + covidDetails + "}"; + } + + private static Covid19BenFeedback storedFeedback() { + Covid19BenFeedback stored = new Covid19BenFeedback(); + stored.setcOVID19ID(BigInteger.valueOf(3)); + return stored; + } + + @Nested + @DisplayName("saving nurse data") + class NurseSave { + + @Test + void saveCovid19NurseData_ignoresARequestWithoutVisitDetails() throws Exception { + assertNull(service.saveCovid19NurseData(null, "auth")); + assertNull(service.saveCovid19NurseData(json("{}"), "auth")); + } + + @Test + void saveCovid19NurseData_skipsTheVisitWhenTheNurseAlreadySavedThisFlow() throws Exception { + when(beneficiaryFlowStatusRepo.checkExistData(any(), any())).thenReturn(new BeneficiaryFlowStatus()); + + assertEquals(0L, service.saveCovid19NurseData(json("{" + visitDetailsBlock("null") + "}"), "auth")); + verify(commonNurseServiceImpl, never()).saveBeneficiaryVisitDetails(any()); + } + + @Test + void saveCovid19NurseData_returnsZeroWhenTheVisitWasNotCreated() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1); + + assertEquals(0L, service.saveCovid19NurseData(json("{" + visitDetailsBlock("null") + "}"), "auth")); + } + + @Test + void saveCovid19NurseData_savesTheScreeningAndAdvancesTheBeneficiaryFlow() throws Exception { + stubVisitCreation(); + when(covid19BenFeedbackRepo.save(any())).thenReturn(storedFeedback()); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1); + + String request = "{" + visitDetailsBlock(covidFeedback()) + ",\"historyDetails\":{}," + + "\"vitalDetails\":{}}"; + + assertEquals(1L, service.saveCovid19NurseData(json(request), "auth")); + } + + @Test + void saveCovid19NurseData_treatsAnAbsentScreeningBlockAsAlreadyDone() throws Exception { + stubVisitCreation(); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1); + + String request = "{" + visitDetailsBlock("null") + ",\"historyDetails\":{},\"vitalDetails\":{}}"; + + assertEquals(1L, service.saveCovid19NurseData(json(request), "auth")); + verify(covid19BenFeedbackRepo, never()).save(any()); + } + + @Test + void saveCovid19NurseData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + stubVisitCreation(); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(0); + + String request = "{" + visitDetailsBlock("null") + ",\"historyDetails\":{},\"vitalDetails\":{}}"; + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.saveCovid19NurseData(json(request), "auth")); + assertTrue(thrown.getMessage().contains("Beneficiary status update failed")); + } + + @Test + void saveCovid19NurseData_failsWhenASectionCouldNotBeSaved() throws Exception { + stubVisitCreation(); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(null); + + String request = "{" + visitDetailsBlock("null") + ",\"historyDetails\":{},\"vitalDetails\":{}}"; + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.saveCovid19NurseData(json(request), "auth")); + assertEquals("Error occurred while saving data", thrown.getMessage()); + } + + private void stubVisitCreation() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + } + + private String covidFeedback() { + return "{\"symptom\":[\"Fever\",\"Cough\"],\"contactStatus\":[\"Household\"]," + + "\"travelList\":[\"Domestic\"],\"recommendation\":[[\"Isolate\",\"Test\"]]," + + "\"suspectedStatusUI\":\"YES\"}"; + } + + @Test + void saveCovid19NurseData_joinsEveryMultiValuedScreeningAnswerBeforeSaving() throws Exception { + stubVisitCreation(); + when(covid19BenFeedbackRepo.save(any())).thenReturn(storedFeedback()); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1); + + String request = "{" + visitDetailsBlock(covidFeedback()) + ",\"historyDetails\":{}," + + "\"vitalDetails\":{}}"; + service.saveCovid19NurseData(json(request), "auth"); + + ArgumentCaptor saved = ArgumentCaptor.forClass(Covid19BenFeedback.class); + verify(covid19BenFeedbackRepo).save(saved.capture()); + assertEquals("Fever||Cough", saved.getValue().getSymptoms_db()); + assertEquals("Household", saved.getValue().getcOVID19_contact_history()); + assertEquals("Domestic", saved.getValue().getTravelType()); + assertEquals("Isolate||Test", saved.getValue().getRecommendation_db()); + assertEquals(Boolean.TRUE, saved.getValue().getSuspectedStatus()); + } + + @Test + void saveCovid19NurseData_recordsANegativeScreeningResult() throws Exception { + stubVisitCreation(); + when(covid19BenFeedbackRepo.save(any())).thenReturn(storedFeedback()); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1); + + String request = "{" + visitDetailsBlock("{\"suspectedStatusUI\":\"NO\"}") + + ",\"historyDetails\":{},\"vitalDetails\":{}}"; + service.saveCovid19NurseData(json(request), "auth"); + + ArgumentCaptor saved = ArgumentCaptor.forClass(Covid19BenFeedback.class); + verify(covid19BenFeedbackRepo).save(saved.capture()); + assertEquals(Boolean.FALSE, saved.getValue().getSuspectedStatus()); + } + + @Test + void saveBenVisitDetails_returnsNothingWhenTheVisitBlockIsMissing() throws Exception { + assertTrue(service.saveBenVisitDetails(json("{}"), new CommonUtilityClass()).isEmpty()); + assertTrue(service.saveBenVisitDetails(null, new CommonUtilityClass()).isEmpty()); + } + } + + @Nested + @DisplayName("saving the individual nurse sections") + class NurseSections { + + @Test + void saveBenCovid19HistoryDetails_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenCovid19HistoryDetails(json("{}"), 1L, 2L)); + assertEquals(1L, service.saveBenCovid19HistoryDetails(null, 1L, 2L)); + } + + @Test + void saveBenCovid19HistoryDetails_savesEverySectionThatWasSent() throws Exception { + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenComorbidConditions(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenMedicationHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveFemaleObstetricHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenMenstrualHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenFamilyHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePersonalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.saveAllergyHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildOptionalVaccineDetail(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveImmunizationHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildDevelopmentHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildFeedingHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePerinatalHistory(any())).thenReturn(1L); + + String history = "{\"pastHistory\":{},\"comorbidConditions\":{}," + + "\"medicationHistory\":{\"medicationHistoryList\":[{\"currentMedication\":\"x\"}]}," + + "\"femaleObstetricHistory\":{},\"menstrualHistory\":{},\"familyHistory\":{}," + + "\"personalHistory\":{},\"childVaccineDetails\":{},\"immunizationHistory\":{}," + + "\"developmentHistory\":{},\"feedingHistory\":{},\"perinatalHistroy\":{}}"; + + assertEquals(1L, service.saveBenCovid19HistoryDetails(json(history), 1L, 2L)); + } + + @Test + void saveBenCovid19HistoryDetails_reportsFailureWhenASectionCouldNotBeSaved() throws Exception { + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(0L); + assertNull(service.saveBenCovid19HistoryDetails(json("{\"pastHistory\":{}}"), 1L, 2L)); + } + + @Test + void saveBenCovid19VitalDetails_savesAnthropometryAndVitalsTogether() throws Exception { + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(5L); + + assertEquals(4L, service.saveBenCovid19VitalDetails(json("{}"), 1L, 2L)); + } + + @Test + void saveBenCovid19VitalDetails_reportsFailureWhenTheVitalsCouldNotBeSaved() throws Exception { + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(null); + + assertNull(service.saveBenCovid19VitalDetails(json("{}"), 1L, 2L)); + assertNull(service.saveBenCovid19VitalDetails(null, 1L, 2L)); + } + } + + @Nested + @DisplayName("reading the nurse and doctor case sheets") + class Reads { + + @Test + void getBenVisitDetailsFrmNurseCovid19_splitsTheStoredScreeningAnswersBackIntoLists() throws Exception { + Covid19BenFeedback stored = new Covid19BenFeedback(); + stored.setSymptoms_db("Fever||Cough"); + stored.setTravelType("Domestic||International"); + stored.setcOVID19_contact_history("Household"); + stored.setRecommendation_db("Isolate||Test"); + stored.setSuspectedStatus(true); + when(covid19BenFeedbackRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(stored); + + String result = service.getBenVisitDetailsFrmNurseCovid19(1L, 2L); + + assertTrue(result.contains("covid19NurseVisitDetail")); + assertEquals(2, stored.getSymptoms().length); + assertEquals(2, stored.getTravelList().length); + assertEquals(1, stored.getContactStatus().length); + assertEquals(1, stored.getRecommendation().size()); + assertEquals("YES", stored.getSuspectedStatusUI()); + } + + @Test + void getBenVisitDetailsFrmNurseCovid19_marksANegativeScreeningForTheUi() throws Exception { + Covid19BenFeedback stored = new Covid19BenFeedback(); + stored.setSuspectedStatus(false); + when(covid19BenFeedbackRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(stored); + + service.getBenVisitDetailsFrmNurseCovid19(1L, 2L); + + assertEquals("NO", stored.getSuspectedStatusUI()); + } + + @Test + void getBenVisitDetailsFrmNurseCovid19_tolratesABeneficiaryWithNoScreeningOnRecord() throws Exception { + when(covid19BenFeedbackRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(null); + assertTrue(service.getBenVisitDetailsFrmNurseCovid19(1L, 2L).contains("covidDetails")); + } + + @Test + void getBenCovidNurseData_combinesTheScreeningVitalsAndHistory() { + String result = service.getBenCovidNurseData(1L, 2L); + + assertTrue(result.contains("covidDetails")); + assertTrue(result.contains("vitals")); + assertTrue(result.contains("history")); + } + + @Test + void getBenCovid19HistoryDetails_gathersEveryHistorySection() { + when(commonNurseServiceImpl.getPastHistoryData(1L, 2L)) + .thenReturn(new com.iemr.mmu.data.anc.BenMedHistory()); + + assertTrue(service.getBenCovid19HistoryDetails(1L, 2L).contains("PastHistory")); + verify(commonNurseServiceImpl).getFeedingHistory(1L, 2L); + } + + @Test + void getBeneficiaryVitalDetails_gathersAnthropometryAndVitals() { + when(commonNurseServiceImpl.getBeneficiaryPhysicalAnthropometryDetails(1L, 2L)).thenReturn("a"); + when(commonNurseServiceImpl.getBeneficiaryPhysicalVitalDetails(1L, 2L)).thenReturn("v"); + + assertTrue(service.getBeneficiaryVitalDetails(1L, 2L).contains("benAnthropometryDetail")); + } + + @Test + void getBenCaseRecordFromDoctorCovid19_readsTheStoredDiagnosisWhenOneExists() throws Exception { + PrescriptionDetail prescription = new PrescriptionDetail(); + prescription.setDiagnosisProvided("Covid-19"); + prescription.setPrescriptionID(7L); + ArrayList prescriptions = new ArrayList<>( + Collections.singletonList(prescription)); + when(prescriptionDetailRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(prescriptions); + when(labTechnicianServiceImpl.getLabResultDataForBen(1L, 2L)).thenReturn(new ArrayList<>()); + when(commonNurseServiceImpl.getGraphicalTrendData(1L, "ncdCare")).thenReturn(new HashMap<>()); + + String result = service.getBenCaseRecordFromDoctorCovid19(1L, 2L); + + assertTrue(result.contains("Covid-19")); + assertTrue(result.contains("GraphData")); + } + + @Test + void getBenCaseRecordFromDoctorCovid19_returnsAnEmptyDiagnosisWhenNoneWasRecorded() throws Exception { + when(prescriptionDetailRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(new ArrayList<>()); + when(labTechnicianServiceImpl.getLabResultDataForBen(1L, 2L)).thenReturn(new ArrayList<>()); + when(commonNurseServiceImpl.getGraphicalTrendData(1L, "ncdCare")).thenReturn(new HashMap<>()); + + assertTrue(service.getBenCaseRecordFromDoctorCovid19(1L, 2L).contains("diagnosis")); + } + } + + @Nested + @DisplayName("updating nurse data") + class NurseUpdates { + + @Test + void updateBenHistoryDetails_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1, service.updateBenHistoryDetails(json("{}"))); + } + + @Test + void updateBenHistoryDetails_updatesEverySectionThatWasSent() throws Exception { + when(commonNurseServiceImpl.updateBenPastHistoryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenComorbidConditions(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenMedicationHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenPersonalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenAllergicHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenFamilyHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateMenstrualHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePastObstetricHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildImmunizationDetail(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildOptionalVaccineDetail(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildFeedingHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePerinatalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildDevelopmentHistory(any())).thenReturn(1); + + String history = "{\"pastHistory\":{},\"comorbidConditions\":{},\"medicationHistory\":{}," + + "\"personalHistory\":{},\"familyHistory\":{},\"menstrualHistory\":{}," + + "\"femaleObstetricHistory\":{},\"immunizationHistory\":{\"benChildVaccineDetails\":[{}]}," + + "\"childVaccineDetails\":{},\"feedingHistory\":{},\"perinatalHistroy\":{}," + + "\"developmentHistory\":{}}"; + + assertEquals(1, service.updateBenHistoryDetails(json(history))); + } + + @Test + void updateBenHistoryDetails_reportsFailureWhenASectionCouldNotBeUpdated() throws Exception { + when(commonNurseServiceImpl.updateBenPastHistoryDetails(any())).thenReturn(0); + assertEquals(0, service.updateBenHistoryDetails(json("{\"pastHistory\":{}}"))); + } + + @Test + void updateBenVitalDetails_updatesAnthropometryAndVitalsTogether() throws Exception { + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(1); + + assertEquals(1, service.updateBenVitalDetails(json("{}"))); + } + + @Test + void updateBenVitalDetails_reportsFailureWhenTheVitalsCouldNotBeUpdated() throws Exception { + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(0); + + assertEquals(0, service.updateBenVitalDetails(json("{}"))); + assertEquals(1, service.updateBenVitalDetails(null)); + } + } + + @Nested + @DisplayName("saving and updating doctor data") + class DoctorData { + + private String doctorRequest(boolean isSpecialist) { + return "{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"providerServiceMapID\":4," + + "\"createdBy\":\"doctor\",\"isSpecialist\":" + isSpecialist + ",\"findings\":{}," + + "\"investigation\":{\"laboratoryList\":[{}]}," + + "\"diagnosis\":{\"doctorDiagnosis\":\"Covid-19\",\"specialistDiagnosis\":\"Isolate\"," + + "\"prescriptionID\":7},\"prescription\":[{\"drugID\":1}],\"refer\":{}}"; + } + + private Map drugResult() { + Map result = new HashMap<>(); + result.put("count", 1); + result.put("prescribedDrugIDs", Collections.singletonList(9L)); + return result; + } + + @Test + void saveDoctorData_savesEverySectionAndAdvancesTheFlow() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.savePrescriptionDetailsCovid19(any(), any(), any(), any(), any(), any(), any(), + any(), anyString())).thenReturn(7L); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.saveDoctorData(json(doctorRequest(false)), "auth")); + } + + @Test + void saveDoctorData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + when(commonNurseServiceImpl.savePrescriptionDetailsCovid19(any(), any(), any(), any(), any(), any(), any(), + any(), any())).thenReturn(7L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.saveDoctorData(json("{\"investigation\":{}}"), "auth")); + } + + @Test + void saveDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.savePrescriptionDetailsCovid19(any(), any(), any(), any(), any(), any(), any(), + any(), anyString())).thenReturn(7L); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, () -> service.saveDoctorData(json(doctorRequest(false)), "auth")); + } + + @Test + void saveDoctorData_failsWhenASectionCouldNotBeSaved() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(0); + when(commonNurseServiceImpl.savePrescriptionDetailsCovid19(any(), any(), any(), any(), any(), any(), any(), + any(), anyString())).thenReturn(7L); + + assertThrows(RuntimeException.class, () -> service.saveDoctorData(json(doctorRequest(false)), "auth")); + } + + @Test + void updateCovid19DoctorData_recordsTheDoctorDiagnosisForANonSpecialist() throws Exception { + stubSuccessfulDoctorUpdate(); + + assertEquals(1L, service.updateCovid19DoctorData(json(doctorRequest(false)), "auth")); + + ArgumentCaptor updated = ArgumentCaptor.forClass(PrescriptionDetail.class); + verify(commonNurseServiceImpl).updatePrescription(updated.capture()); + assertEquals("Covid-19", updated.getValue().getDiagnosisProvided()); + } + + @Test + void updateCovid19DoctorData_recordsTheSpecialistInstructionForASpecialist() throws Exception { + stubSuccessfulDoctorUpdate(); + + assertEquals(1L, service.updateCovid19DoctorData(json(doctorRequest(true)), "auth")); + + ArgumentCaptor updated = ArgumentCaptor.forClass(PrescriptionDetail.class); + verify(commonNurseServiceImpl).updatePrescription(updated.capture()); + assertEquals("Isolate", updated.getValue().getInstruction()); + } + + @Test + void updateCovid19DoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + stubSuccessfulDoctorUpdate(); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(0); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.updateCovid19DoctorData(json(doctorRequest(false)), "auth")); + assertTrue(thrown.getMessage().contains("Beneficiary status update failed")); + } + + @Test + void updateCovid19DoctorData_failsWhenASectionCouldNotBeUpdated() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(0); + + assertThrows(RuntimeException.class, + () -> service.updateCovid19DoctorData(json(doctorRequest(false)), "auth")); + } + + private void stubSuccessfulDoctorUpdate() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImplTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImplTest.java new file mode 100644 index 00000000..482b415e --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerImplTest.java @@ -0,0 +1,208 @@ +/* +* 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.mmu.service.dataSyncActivity; + +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.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +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.RestTemplate; + +import com.iemr.mmu.data.syncActivity_syncLayer.SyncDownloadMaster; +import com.iemr.mmu.data.syncActivity_syncLayer.TempVan; +import com.iemr.mmu.repo.syncActivity_syncLayer.SyncDownloadMasterRepo; +import com.iemr.mmu.repo.syncActivity_syncLayer.TempVanRepo; +import com.iemr.mmu.utils.CookieUtil; + +class DownloadDataFromServerImplTest { + + @Mock + private SyncDownloadMasterRepo syncDownloadMasterRepo; + @Mock + private DataSyncRepository dataSyncRepository; + @Mock + private TempVanRepo tempVanRepo; + @Mock + private CookieUtil cookieUtil; + + @InjectMocks + private DownloadDataFromServerImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(service, "dataSyncDownloadUrl", "http://central/master"); + ReflectionTestUtils.setField(service, "benGenUrlCentral", "http://central/benGen"); + ReflectionTestUtils.setField(service, "benImportUrlLocal", "http://local/benImport"); + // The download counters are static, so each test starts from a settled state. + ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "progressCounter", 0); + ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "totalCounter", 0); + ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "failedCounter", 0); + ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "failedMasters", new StringBuilder()); + } + + private ArrayList oneMasterTable() { + SyncDownloadMaster master = new SyncDownloadMaster(); + master.setDownloadMasterTableID(1); + master.setSchemaName("db_iemr"); + master.setTableName("m_gender"); + master.setVanColumnName("GenderID,GenderName"); + ArrayList masters = new ArrayList<>(); + masters.add(master); + return masters; + } + + private MockedConstruction centralAnswering(String body) { + return mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(body, HttpStatus.OK))); + } + + @Test + @DisplayName("a master download is accepted and the progress counters are reset") + void downloadMasterDataFromServer_acceptsTheRequestAndResetsTheProgressCounters() throws Exception { + when(syncDownloadMasterRepo.getDownloadTables()).thenReturn(oneMasterTable()); + + assertEquals(" Master download started ", service.downloadMasterDataFromServer("auth", "token", 1, 2)); + + // The download itself runs on its own thread pool; what the caller sees is the + // reset progress counters. + assertEquals(1, ReflectionTestUtils.getField(DownloadDataFromServerImpl.class, "totalCounter")); + assertEquals(0, ReflectionTestUtils.getField(DownloadDataFromServerImpl.class, "failedCounter")); + } + + @Test + @DisplayName("a second download request while one is running reports that it is in progress") + void downloadMasterDataFromServer_reportsADownloadThatIsStillRunning() throws Exception { + ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "totalCounter", 5); + ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "progressCounter", 1); + + assertEquals("inProgress", service.downloadMasterDataFromServer("auth", "token", 1, 2)); + } + + @Test + @DisplayName("the download progress is reported as a percentage") + void getDownloadStatus_reportsTheProgressAsAPercentage() { + ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "totalCounter", 4); + ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "progressCounter", 1); + ReflectionTestUtils.setField(DownloadDataFromServerImpl.class, "failedCounter", 2); + + Map status = service.getDownloadStatus(); + + assertEquals(25.0, status.get("percentage")); + assertEquals(2, status.get("failedMasterCount")); + } + + @Test + @DisplayName("the van assigned to this installation is reported") + void getVanDetailsForMasterDownload_reportsTheOnlyVanOfThisInstallation() throws Exception { + TempVan van = new TempVan(); + van.setVanID(1); + when(tempVanRepo.getVanID()).thenReturn(new ArrayList<>(Collections.singletonList(van))); + + assertTrue(service.getVanDetailsForMasterDownload().contains("\"vanID\":1")); + } + + @Test + @DisplayName("more than one configured van is reported as a configuration error") + void getVanDetailsForMasterDownload_failsWhenMoreThanOneVanIsConfigured() { + when(tempVanRepo.getVanID()).thenReturn(new ArrayList<>(List.of(new TempVan(), new TempVan()))); + + Exception thrown = assertThrows(Exception.class, () -> service.getVanDetailsForMasterDownload()); + assertTrue(thrown.getMessage().contains("more than 1 van")); + } + + @Test + @DisplayName("a generated beneficiary id block is imported into the van") + void callCentralAPIToGenerateBenIDAndimportToLocal_importsTheGeneratedIdBlock() throws Exception { + String central = "{\"statusCode\":200,\"data\":[{\"beneficiaryID\":1}]}"; + String local = "{\"statusCode\":200,\"data\":{}}"; + + try (MockedConstruction rest = mockConstruction(RestTemplate.class, (mock, context) -> { + when(mock.exchange(eq("http://central/benGen"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(central, HttpStatus.OK)); + when(mock.exchange(eq("http://local/benImport"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(local, HttpStatus.OK)); + })) { + assertEquals(2, service.callCentralAPIToGenerateBenIDAndimportToLocal("{}", "auth", "serverAuth", "token")); + } + } + + @Test + @DisplayName("a block the van could not import stops short of a completed import") + void callCentralAPIToGenerateBenIDAndimportToLocal_stopsShortWhenTheVanCannotImport() throws Exception { + String central = "{\"statusCode\":200,\"data\":[{\"beneficiaryID\":1}]}"; + + try (MockedConstruction rest = mockConstruction(RestTemplate.class, (mock, context) -> { + when(mock.exchange(eq("http://central/benGen"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(central, HttpStatus.OK)); + when(mock.exchange(eq("http://local/benImport"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>("{\"statusCode\":500}", HttpStatus.OK)); + })) { + assertEquals(1, service.callCentralAPIToGenerateBenIDAndimportToLocal("{}", "auth", "serverAuth", "token")); + } + } + + @Test + @DisplayName("a central server that generates no ids leaves nothing to import") + void callCentralAPIToGenerateBenIDAndimportToLocal_leavesNothingToImportWhenNoIdWasGenerated() throws Exception { + try (MockedConstruction rest = centralAnswering("{\"statusCode\":500}")) { + assertEquals(0, service.callCentralAPIToGenerateBenIDAndimportToLocal("{}", "auth", "serverAuth", "token")); + } + } + + @Test + @DisplayName("a central server that cannot be reached is reported to the caller") + void callCentralAPIToGenerateBenIDAndimportToLocal_reportsACentralServerThatCannotBeReached() { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenThrow(new RuntimeException("connection refused")))) { + + Exception thrown = assertThrows(Exception.class, () -> service + .callCentralAPIToGenerateBenIDAndimportToLocal("{}", "auth", "serverAuth", "token")); + assertTrue(thrown.getMessage().contains("Error while generating")); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImplTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImplTest.java new file mode 100644 index 00000000..26840ce9 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/DownloadDataFromServerTransactionalImplTest.java @@ -0,0 +1,169 @@ +/* +* 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.mmu.service.dataSyncActivity; + +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.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +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.RestTemplate; + +import com.iemr.mmu.repo.login.MasterVanRepo; +import com.iemr.mmu.repo.syncActivity_syncLayer.IndentIssueRepo; +import com.iemr.mmu.repo.syncActivity_syncLayer.IndentOrderRepo; +import com.iemr.mmu.repo.syncActivity_syncLayer.IndentRepo; +import com.iemr.mmu.repo.syncActivity_syncLayer.ItemStockEntryRepo; +import com.iemr.mmu.repo.syncActivity_syncLayer.StockTransferRepo; +import com.iemr.mmu.utils.CookieUtil; + +class DownloadDataFromServerTransactionalImplTest { + + @Mock + private MasterVanRepo masterVanRepo; + @Mock + private IndentRepo indentRepo; + @Mock + private IndentOrderRepo indentOrderRepo; + @Mock + private IndentIssueRepo indentIssueRepo; + @Mock + private StockTransferRepo stockTransferRepo; + @Mock + private ItemStockEntryRepo itemStockEntryRepo; + @Mock + private CookieUtil cookieUtil; + + @InjectMocks + private DownloadDataFromServerTransactionalImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(service, "dataSyncTransactionDownloadUrl", "http://central/download"); + ReflectionTestUtils.setField(service, "dataSyncProcessedFlagUpdate", "http://central/flag"); + } + + /** The central server's download payload plus its acknowledgement of the flag update. */ + private MockedConstruction centralAnswering(String downloadBody) { + return mockConstruction(RestTemplate.class, (mock, context) -> { + when(mock.exchange(eq("http://central/download"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(downloadBody, HttpStatus.OK)); + when(mock.exchange(eq("http://central/flag"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>("{\"statusCode\":200,\"data\":{}}", HttpStatus.OK)); + }); + } + + @Test + @DisplayName("an empty download leaves the van's stock tables untouched") + void downloadTransactionalData_leavesTheStockTablesUntouchedWhenThereIsNothingToDownload() throws Exception { + when(masterVanRepo.getFacilityID(1)).thenReturn(5); + + try (MockedConstruction rest = centralAnswering("{\"data\":[]}")) { + assertEquals(1, service.downloadTransactionalData(1, "auth", "token")); + } + verify(indentRepo, never()).saveAll(any()); + } + + @Test + @DisplayName("a downloaded indent is stored against the van's own row when one already exists") + void downloadTransactionalData_reusesTheVanRowOfAnAlreadyDownloadedIndent() throws Exception { + when(masterVanRepo.getFacilityID(1)).thenReturn(5); + when(indentRepo.searchBySyncFacilityIDAndVanSerialNo(anyInt(), anyLong())).thenReturn(9L); + when(indentIssueRepo.searchBySyncFacilityIDAndVanSerialNo(anyInt(), anyLong())).thenReturn(null); + when(itemStockEntryRepo.searchBySyncFacilityIDAndVanSerialNo(anyInt(), anyLong())).thenReturn(null); + + String payload = "{\"data\":[{\"indentID\":1,\"indentIssueID\":1,\"itemStockEntryID\":1," + + "\"syncFacilityID\":5,\"vanSerialNo\":2}]}"; + + try (MockedConstruction rest = centralAnswering(payload)) { + assertEquals(1, service.downloadTransactionalData(1, "auth", "token")); + } + verify(indentRepo).saveAll(any()); + verify(indentIssueRepo).saveAll(any()); + verify(itemStockEntryRepo).saveAll(any()); + } + + @Test + @DisplayName("a van without a facility mapping cannot download anything") + void downloadTransactionalData_failsForAVanWithoutAFacilityMapping() { + when(masterVanRepo.getFacilityID(1)).thenReturn(null); + + Exception thrown = assertThrows(Exception.class, () -> service.downloadTransactionalData(1, "auth", "token")); + assertEquals("Facility mapping for this van is either missing/wrong...", thrown.getMessage()); + } + + @Test + @DisplayName("a rejected flag update still leaves the downloaded rows stored") + void downloadTransactionalData_storesTheRowsEvenWhenTheFlagUpdateIsRejected() throws Exception { + when(masterVanRepo.getFacilityID(1)).thenReturn(5); + + String payload = "{\"data\":[{\"indentID\":1,\"syncFacilityID\":5,\"vanSerialNo\":2}]}"; + + try (MockedConstruction rest = mockConstruction(RestTemplate.class, (mock, context) -> { + when(mock.exchange(eq("http://central/download"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(payload, HttpStatus.OK)); + when(mock.exchange(eq("http://central/flag"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>("{\"statusCode\":500}", HttpStatus.OK)); + })) { + assertEquals(1, service.downloadTransactionalData(1, "auth", "token")); + } + verify(indentRepo).saveAll(any()); + } + + @Test + @DisplayName("a flag update with no body still leaves the downloaded rows stored") + void downloadTransactionalData_storesTheRowsEvenWhenTheFlagUpdateHasNoBody() throws Exception { + when(masterVanRepo.getFacilityID(1)).thenReturn(5); + + String payload = "{\"data\":[{\"indentID\":1,\"syncFacilityID\":5,\"vanSerialNo\":2}]}"; + + try (MockedConstruction rest = mockConstruction(RestTemplate.class, (mock, context) -> { + when(mock.exchange(eq("http://central/download"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(payload, HttpStatus.OK)); + when(mock.exchange(eq("http://central/flag"), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(HttpStatus.OK)); + })) { + assertEquals(1, service.downloadTransactionalData(1, "auth", "token")); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImplTest.java b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImplTest.java new file mode 100644 index 00000000..9764af64 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncActivity/UploadDataToServerImplTest.java @@ -0,0 +1,405 @@ +/* +* 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.mmu.service.dataSyncActivity; + +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.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +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.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import com.iemr.mmu.data.syncActivity_syncLayer.DataSyncGroups; +import com.iemr.mmu.data.syncActivity_syncLayer.SyncUtilityClass; +import com.iemr.mmu.repo.syncActivity_syncLayer.DataSyncGroupsRepo; +import com.iemr.mmu.repo.syncActivity_syncLayer.SyncUtilityClassRepo; +import com.iemr.mmu.repo.login.MasterVanRepo; +import com.iemr.mmu.utils.CookieUtil; + +class UploadDataToServerImplTest { + + @Mock + private DataSyncRepository dataSyncRepository; + @Mock + private DataSyncGroupsRepo dataSyncGroupsRepo; + @Mock + private MasterVanRepo masterVanRepo; + @Mock + private SyncUtilityClassRepo syncutilityClassRepo; + @Mock + private CookieUtil cookieUtil; + + @InjectMocks + private UploadDataToServerImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(service, "dataSyncUploadUrl", "http://central/sync"); + ReflectionTestUtils.setField(service, "BATCH_SIZE", 2); + } + + private DataSyncGroups group() { + DataSyncGroups group = new DataSyncGroups(); + group.setSyncTableGroupID(1); + group.setSyncTableGroupName("Beneficiary"); + return group; + } + + private SyncUtilityClass table(String tableName) { + SyncUtilityClass table = new SyncUtilityClass(); + table.setSchemaName("db_iemr"); + table.setTableName(tableName); + table.setVanColumnName("VanSerialNo,BeneficiaryRegID"); + table.setServerColumnName("VanSerialNo,BeneficiaryRegID"); + table.setVanAutoIncColumnName("VanSerialNo"); + return table; + } + + private List> rows(int count) { + List> rows = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + Map row = new HashMap<>(); + row.put("VanSerialNo", i); + rows.add(row); + } + return rows; + } + + /** The central server's per-record acknowledgement for a batch of van serial numbers. */ + private String recordsResponse(boolean... outcomes) { + StringBuilder records = new StringBuilder(); + for (int i = 0; i < outcomes.length; i++) { + if (i > 0) { + records.append(","); + } + records.append("{\"vanSerialNo\":\"").append(i + 1).append("\",\"success\":").append(outcomes[i]) + .append(outcomes[i] ? "" : ",\"reason\":\"duplicate row\"").append("}"); + } + return "{\"statusCode\":200,\"errorMessage\":\"Success\",\"data\":{\"records\":[" + records + "]}}"; + } + + /** A server that acknowledges every row of whatever batch it is sent. */ + private MockedConstruction serverAcknowledgingEveryRow() { + return mockConstruction(RestTemplate.class, (mock, context) -> when( + mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))).thenAnswer(invocation -> { + org.springframework.http.HttpEntity request = invocation.getArgument(2); + com.google.gson.JsonArray syncData = com.google.gson.JsonParser + .parseString(String.valueOf(request.getBody())).getAsJsonObject() + .getAsJsonArray("syncData"); + boolean[] outcomes = new boolean[syncData.size()]; + java.util.Arrays.fill(outcomes, true); + return new ResponseEntity<>(recordsResponse(outcomes), HttpStatus.OK); + })); + } + + private MockedConstruction serverAnswering(String body) { + return mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(body, HttpStatus.OK))); + } + + @Nested + @DisplayName("uploading a van's data") + class Upload { + + @Test + void getDataToSyncToServer_reportsThatThereWasNothingToSync() throws Exception { + when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group()))); + when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false)) + .thenReturn(List.of(table("i_beneficiary"))); + when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString())) + .thenReturn(new ArrayList<>()); + + assertEquals("No data to sync", service.getDataToSyncToServer(1, "nurse", "auth", "token")); + } + + @Test + void getDataToSyncToServer_reportsACompletelySuccessfulSync() throws Exception { + when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group()))); + when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false)) + .thenReturn(List.of(table("i_beneficiary"))); + when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString())) + .thenReturn(rows(2)); + + try (MockedConstruction rest = serverAnswering(recordsResponse(true, true))) { + String response = service.getDataToSyncToServer(1, "nurse", "auth", "token"); + + assertTrue(response.contains("Data sync completed successfully")); + assertTrue(response.contains("\"status\" : \"completed\"")); + } + verify(dataSyncRepository).updateProcessedFlagInVan(eq("db_iemr"), eq("i_beneficiary"), any(), + eq("VanSerialNo"), eq("nurse"), eq("P"), eq("null")); + } + + @Test + void getDataToSyncToServer_reportsAPartiallySuccessfulTable() throws Exception { + when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group()))); + when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false)) + .thenReturn(List.of(table("i_beneficiary"))); + when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString())) + .thenReturn(rows(2)); + + try (MockedConstruction rest = serverAnswering(recordsResponse(true, false))) { + String response = service.getDataToSyncToServer(1, "nurse", "auth", "token"); + + assertTrue(response.contains("\"status\" : \"partial\""), response); + assertTrue(response.contains("\"failedRecords\" : 1"), response); + } + verify(dataSyncRepository).updateProcessedFlagInVan(anyString(), anyString(), any(), anyString(), + anyString(), eq("F"), eq("duplicate row")); + } + + @Test + void getDataToSyncToServer_reportsATableThatFailedEntirely() throws Exception { + when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group()))); + when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false)) + .thenReturn(List.of(table("i_beneficiary"))); + when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString())) + .thenReturn(rows(2)); + + try (MockedConstruction rest = serverAnswering(recordsResponse(false, false))) { + String response = service.getDataToSyncToServer(1, "nurse", "auth", "token"); + + assertTrue(response.contains("Data sync completed with failures"), response); + assertTrue(response.contains("\"status\" : \"failed\""), response); + } + } + + @Test + void getDataToSyncToServer_splitsALargeTableIntoBatchesAndARemainder() throws Exception { + when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group()))); + when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false)) + .thenReturn(List.of(table("i_beneficiary"))); + when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString())) + .thenReturn(rows(3)); + + try (MockedConstruction rest = serverAcknowledgingEveryRow()) { + String response = service.getDataToSyncToServer(1, "nurse", "auth", "token"); + + assertTrue(response.contains("Data sync completed successfully"), response); + assertTrue(response.contains("\"totalRecords\" : 3"), response); + } + } + + @Test + void getDataToSyncToServer_stopsTheTableWhenTheServerReportsAnError() throws Exception { + when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group()))); + when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false)) + .thenReturn(List.of(table("i_beneficiary"))); + when(dataSyncRepository.getDataForGivenSchemaAndTable(anyString(), anyString(), anyString())) + .thenReturn(rows(2)); + + String error = "{\"statusCode\":500,\"errorMessage\":\"Central server unavailable\"}"; + try (MockedConstruction rest = serverAnswering(error)) { + String response = service.getDataToSyncToServer(1, "nurse", "auth", "token"); + + assertTrue(response.contains("Data sync completed with failures"), response); + assertTrue(response.contains("\"status\" : \"failed\""), response); + } + verify(dataSyncRepository).updateProcessedFlagInVan(anyString(), anyString(), any(), anyString(), + anyString(), eq("F"), eq("Central server unavailable")); + } + } + + @Nested + @DisplayName("sending one batch to the central server") + class SendBatch { + + @Test + void syncDataToServer_reportsSuccessForEveryAcknowledgedRecord() throws Exception { + when(masterVanRepo.getFacilityID(1)).thenReturn(5); + + try (MockedConstruction rest = serverAnswering(recordsResponse(true, true))) { + Map result = service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo", + "VanSerialNo", rows(2), "nurse", "auth", "token"); + + assertEquals("Data successfully synced", result.get("status")); + assertEquals(2, result.get("successCount")); + assertEquals(0, result.get("failCount")); + } + } + + @Test + void syncDataToServer_reportsAPartialSuccess() throws Exception { + try (MockedConstruction rest = serverAnswering(recordsResponse(true, false))) { + assertEquals("Partial success", service.syncDataToServer(1, "db_iemr", "i_beneficiary", + "VanSerialNo", "VanSerialNo", rows(2), "nurse", "auth", "token").get("status")); + } + } + + @Test + void syncDataToServer_acceptsTheBeneficiaryIdMappingAcknowledgement() throws Exception { + String body = "{\"statusCode\":200,\"errorMessage\":\"Success\"," + + "\"data\":{\"response\":\"Data sync success\"}}"; + + try (MockedConstruction rest = serverAnswering(body)) { + Map result = service.syncDataToServer(1, "db_iemr", "m_beneficiaryregidmapping", + "VanSerialNo", "VanSerialNo", rows(2), "nurse", "auth", "token"); + + assertEquals("Data successfully synced", result.get("status")); + assertEquals(2, result.get("successCount")); + } + } + + @Test + void syncDataToServer_reportsAFailedBeneficiaryIdMapping() throws Exception { + String body = "{\"statusCode\":200,\"errorMessage\":\"Success\",\"data\":{\"response\":\"rejected\"}}"; + + try (MockedConstruction rest = serverAnswering(body)) { + Map result = service.syncDataToServer(1, "db_iemr", "m_beneficiaryregidmapping", + "VanSerialNo", "VanSerialNo", rows(2), "nurse", "auth", "token"); + + assertEquals("Sync failed", result.get("status")); + assertEquals(2, result.get("failCount")); + } + } + + @Test + void syncDataToServer_reportsAnUnparseableServerResponse() throws Exception { + try (MockedConstruction rest = serverAnswering("not json")) { + Map result = service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo", + "VanSerialNo", rows(2), "nurse", "auth", "token"); + + assertEquals("Sync failed", result.get("status")); + } + verify(dataSyncRepository).updateProcessedFlagInVan(anyString(), anyString(), any(), anyString(), + anyString(), eq("F"), eq("Invalid server response")); + } + + @Test + void syncDataToServer_reportsAnEmptyServerResponse() throws Exception { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(HttpStatus.OK)))) { + + assertEquals("Sync failed", service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo", + "VanSerialNo", rows(2), "nurse", "auth", "token").get("status")); + } + verify(dataSyncRepository).updateProcessedFlagInVan(anyString(), anyString(), any(), anyString(), + anyString(), eq("F"), eq("Empty server response")); + } + + @Test + void syncDataToServer_reportsAServerThatCannotBeReached() throws Exception { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenThrow(new ResourceAccessException("connection refused")))) { + + assertEquals("Sync failed", service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo", + "VanSerialNo", rows(2), "nurse", "auth", "token").get("status")); + } + } + + @Test + void syncDataToServer_reportsAnUnexpectedFailure() throws Exception { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenThrow(new IllegalStateException("boom")))) { + + assertEquals("Sync failed", service.syncDataToServer(1, "db_iemr", "i_beneficiary", "VanSerialNo", + "VanSerialNo", rows(2), "nurse", "auth", "token").get("status")); + } + } + } + + @Nested + @DisplayName("sync helpers") + class Helpers { + + @Test + void getVanAndServerColumnList_readsTheTablesOfAGroup() throws Exception { + List tables = List.of(table("i_beneficiary")); + when(syncutilityClassRepo.findBySyncTableGroupIDAndDeletedOrderBySyncTableDetailID(1, false)) + .thenReturn(tables); + + assertEquals(tables, service.getVanAndServerColumnList(1)); + } + + @Test + void getVanSerialNoListForSyncedData_joinsTheVanSerialNumbersWithCommas() throws Exception { + assertEquals("1,2,3", service.getVanSerialNoListForSyncedData("VanSerialNo", rows(3)).toString()); + assertEquals("", service.getVanSerialNoListForSyncedData("VanSerialNo", new ArrayList<>()).toString()); + } + + @Test + void getDataSyncGroupDetails_serialisesTheConfiguredGroups() { + when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(new ArrayList<>(List.of(group()))); + assertTrue(service.getDataSyncGroupDetails().contains("Beneficiary")); + + when(dataSyncGroupsRepo.findByDeleted(false)).thenReturn(null); + assertNull(service.getDataSyncGroupDetails()); + } + + @Test + void syncResult_carriesTheOutcomeOfOneTable() { + SyncResult result = new SyncResult("db_iemr", "i_beneficiary", "1", "nurse", false, "duplicate row"); + + assertEquals("db_iemr", result.getSchemaName()); + assertEquals("i_beneficiary", result.getTableName()); + assertEquals("1", result.getVanSerialNo()); + assertEquals("nurse", result.getSyncedBy()); + assertEquals(false, result.isSuccess()); + assertEquals("duplicate row", result.getReason()); + + result.setSuccess(true); + assertEquals(true, result.isSuccess()); + + SyncResult same = new SyncResult("db_iemr", "i_beneficiary", "1", "nurse", true, "duplicate row"); + assertEquals(same, result); + assertEquals(same.hashCode(), result.hashCode()); + assertTrue(result.toString().contains("i_beneficiary")); + + result.setSchemaName("db_identity"); + result.setTableName("m_beneficiaryregidmapping"); + result.setVanSerialNo("2"); + result.setSyncedBy("doctor"); + result.setReason(null); + org.junit.jupiter.api.Assertions.assertNotEquals(same, result); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralQueryTest.java b/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralQueryTest.java new file mode 100644 index 00000000..16c4b099 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/DataSyncRepositoryCentralQueryTest.java @@ -0,0 +1,391 @@ +/* +* 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.mmu.service.dataSyncLayerCentral; + +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.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.mmu.data.syncActivity_syncLayer.SyncUploadDataDigester; + +class DataSyncRepositoryCentralQueryTest { + + private DataSource dataSource; + + @BeforeEach + void setUp() { + dataSource = Mockito.mock(DataSource.class); + } + + private List> oneRow() { + Map row = new HashMap<>(); + row.put("VanSerialNo", 1); + return new ArrayList<>(Collections.singletonList(row)); + } + + /** A JdbcTemplate that answers every read with the given rows. */ + private MockedConstruction databaseReturning(List> rows) { + return mockConstruction(JdbcTemplate.class, (mock, context) -> { + when(mock.queryForList(anyString(), org.mockito.ArgumentMatchers.any(Object[].class))).thenReturn(rows); + when(mock.queryForList(anyString())).thenReturn(rows); + + when(mock.batchUpdate(anyString(), any(List.class))).thenReturn(new int[] { 1 }); + }); + } + + private MockedConstruction databaseFailing() { + return mockConstruction(JdbcTemplate.class, (mock, context) -> { + when(mock.queryForList(anyString(), org.mockito.ArgumentMatchers.any(Object[].class))) + .thenThrow(new RuntimeException("db down")); + when(mock.queryForList(anyString())).thenThrow(new RuntimeException("db down")); + + when(mock.batchUpdate(anyString(), any(List.class))).thenThrow(new RuntimeException("db down")); + }); + } + + @Nested + @DisplayName("DataSyncRepositoryCentral") + class Central { + + private DataSyncRepositoryCentral repository; + + @BeforeEach + void setUp() { + repository = new DataSyncRepositoryCentral(); + ReflectionTestUtils.setField(repository, "dataSource", dataSource); + } + + @Test + void checkRecordIsAlreadyPresentOrNot_matchesAClinicalRowOnItsVan() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertEquals(1, repository.checkRecordIsAlreadyPresentOrNot("db_iemr", "t_benvisitdetail", "1", "2", + "VanSerialNo", 0)); + + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(db.constructed().get(0)).queryForList(query.capture(), org.mockito.ArgumentMatchers.any(Object[].class)); + assertTrue(query.getValue().endsWith("AND VanID = ?"), query.getValue()); + } + } + + @Test + void checkRecordIsAlreadyPresentOrNot_matchesAStockRowOnItsFacility() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertEquals(1, repository.checkRecordIsAlreadyPresentOrNot("db_iemr", "t_indent", "1", "2", + "VanSerialNo", 5)); + + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(db.constructed().get(0)).queryForList(query.capture(), org.mockito.ArgumentMatchers.any(Object[].class)); + assertTrue(query.getValue().endsWith("AND SyncFacilityID = ?"), query.getValue()); + } + } + + @Test + void checkRecordIsAlreadyPresentOrNot_reportsARowThatIsNotOnTheServerYet() { + try (MockedConstruction db = databaseReturning(new ArrayList<>())) { + assertEquals(0, repository.checkRecordIsAlreadyPresentOrNot("db_iemr", "t_benvisitdetail", "1", "2", + "VanSerialNo", 0)); + } + } + + @Test + void checkRecordIsAlreadyPresentOrNot_rejectsAnIdentifierThatIsNotWhitelisted() { + assertThrows(IllegalArgumentException.class, () -> repository + .checkRecordIsAlreadyPresentOrNot("evil_schema", "t_benvisitdetail", "1", "2", "VanSerialNo", 0)); + assertThrows(IllegalArgumentException.class, () -> repository + .checkRecordIsAlreadyPresentOrNot("db_iemr", "evil_table", "1", "2", "VanSerialNo", 0)); + assertThrows(IllegalArgumentException.class, () -> repository + .checkRecordIsAlreadyPresentOrNot("db_iemr", "t_benvisitdetail", "1", "2", "van;drop", 0)); + } + + @Test + void checkRecordIsAlreadyPresentOrNot_reportsADatabaseThatCannotBeRead() { + try (MockedConstruction db = databaseFailing()) { + RuntimeException thrown = assertThrows(RuntimeException.class, () -> repository + .checkRecordIsAlreadyPresentOrNot("db_iemr", "t_benvisitdetail", "1", "2", "VanSerialNo", 0)); + assertTrue(thrown.getMessage().contains("Failed to check record existence")); + } + } + + @Test + void syncDataToCentralDB_appliesTheBatchAndReportsTheRowCounts() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertArrayEquals(new int[] { 1 }, repository.syncDataToCentralDB("db_iemr", "t_benvisitdetail", + "VanSerialNo", "INSERT INTO db_iemr.t_benvisitdetail(VanSerialNo) VALUES (?)", + new ArrayList<>(Collections.singletonList(new Object[] { 1 })))); + } + } + + @Test + void syncDataToCentralDB_reportsABatchTheDatabaseRejected() { + try (MockedConstruction db = databaseFailing()) { + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> repository.syncDataToCentralDB("db_iemr", "t_benvisitdetail", "VanSerialNo", "INSERT", + new ArrayList<>(Collections.singletonList(new Object[] { 1 })))); + assertTrue(thrown.getMessage().contains("Batch sync failed")); + } + } + + @Test + void getMasterDataFromTable_readsEveryRowOfAnAllVanMaster() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertEquals(1, repository.getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo,VanID", "A", + null, 1, 2).size()); + } + } + + @Test + void getMasterDataFromTable_narrowsAVanMasterToItsVan() { + try (MockedConstruction db = databaseReturning(oneRow())) { + repository.getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", "V", null, 1, 2); + + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(db.constructed().get(0)).queryForList(query.capture(), org.mockito.ArgumentMatchers.any(Object[].class)); + assertTrue(query.getValue().endsWith("WHERE VanID = ?"), query.getValue()); + } + } + + @Test + void getMasterDataFromTable_narrowsAProviderMasterToItsProvider() { + try (MockedConstruction db = databaseReturning(oneRow())) { + repository.getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", "P", null, 1, 2); + + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(db.constructed().get(0)).queryForList(query.capture(), org.mockito.ArgumentMatchers.any(Object[].class)); + assertTrue(query.getValue().endsWith("WHERE ProviderServiceMapID = ?"), query.getValue()); + } + } + + @Test + void getMasterDataFromTable_readsOnlyWhatChangedSinceTheLastDownload() { + Timestamp lastDownload = Timestamp.valueOf("2024-01-01 00:00:00"); + + try (MockedConstruction db = databaseReturning(oneRow())) { + repository.getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", "V", lastDownload, 1, 2); + repository.getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", "P", lastDownload, 1, 2); + repository.getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", "A", lastDownload, 1, 2); + + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(db.constructed().get(0), Mockito.times(3)).queryForList(query.capture(), + org.mockito.ArgumentMatchers.any(Object[].class)); + assertTrue(query.getAllValues().get(2).endsWith("WHERE LastModDate >= ?"), query.getValue()); + } + } + + @Test + void getMasterDataFromTable_readsTheWholeTableWhenNoMasterTypeWasGiven() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertEquals(1, repository + .getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", null, null, 1, 2).size()); + } + } + + @Test + void getMasterDataFromTable_rejectsADateFormattedColumn() { + // The column list is split on commas first, so a date_format column is never + // seen whole: with its format argument the format arrives as a column name of + // its own, and without one the closing bracket fails the identifier check. + assertThrows(IllegalArgumentException.class, () -> repository.getMasterDataFromTable("db_iemr", + "t_benvisitdetail", "date_format(CreatedDate,\'%Y\')", "A", null, 1, 2)); + assertThrows(IllegalArgumentException.class, () -> repository.getMasterDataFromTable("db_iemr", + "t_benvisitdetail", "date_format(CreatedDate)", "A", null, 1, 2)); + } + + @Test + void getMasterDataFromTable_rejectsAnIdentifierThatIsNotWhitelisted() { + assertThrows(IllegalArgumentException.class, + () -> repository.getMasterDataFromTable("evil", "t_benvisitdetail", "VanSerialNo", "A", null, 1, 2)); + assertThrows(IllegalArgumentException.class, + () -> repository.getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo;drop", "A", null, 1, 2)); + assertThrows(IllegalArgumentException.class, + () -> repository.getMasterDataFromTable("db_iemr", "t_benvisitdetail", " ", "A", null, 1, 2)); + } + + @Test + void getMasterDataFromTable_reportsADatabaseThatCannotBeRead() { + try (MockedConstruction db = databaseFailing()) { + RuntimeException thrown = assertThrows(RuntimeException.class, () -> repository + .getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", "A", null, 1, 2)); + assertTrue(thrown.getMessage().contains("Failed to fetch master data")); + } + } + + private SyncUploadDataDigester digester(String schema, String table, String columns) { + SyncUploadDataDigester digester = new SyncUploadDataDigester(); + digester.setSchemaName(schema); + digester.setTableName(table); + digester.setServerColumns(columns); + return digester; + } + + @Test + void getBatchForBenDetails_readsOnePageOfBeneficiaryDetails() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertEquals(1, repository.getBatchForBenDetails( + digester("db_identity", "i_beneficiarydetails", "BeneficiaryDetailsId"), " WHERE VanID = 1 ", + 10, 0).size()); + } + } + + @Test + void getBatchForBenDetails_rejectsAnIdentifierThatIsNotWhitelisted() { + assertThrows(IllegalArgumentException.class, () -> repository + .getBatchForBenDetails(digester("evil", "i_beneficiarydetails", "Id"), " WHERE 1=1 ", 10, 0)); + } + + @Test + void getBatchForBenDetails_reportsADatabaseThatCannotBeRead() { + try (MockedConstruction db = databaseFailing()) { + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> repository.getBatchForBenDetails( + digester("db_identity", "i_beneficiarydetails", "BeneficiaryDetailsId"), + " WHERE VanID = 1 ", 10, 0)); + assertTrue(thrown.getMessage().contains("Failed to fetch batch data")); + } + } + } + + @Nested + @DisplayName("DataSyncRepositoryCentralDownload") + class Download { + + private DataSyncRepositoryCentralDownload repository; + + @BeforeEach + void setUp() { + repository = new DataSyncRepositoryCentralDownload(); + ReflectionTestUtils.setField(repository, "dataSource", dataSource); + } + + @Test + void checkRecordIsAlreadyPresentOrNot_matchesAClinicalRowOnItsVan() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertEquals(1, repository.checkRecordIsAlreadyPresentOrNot("db_iemr", "t_benvisitdetail", "1", "2", + "VanSerialNo", 0)); + + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(db.constructed().get(0)).queryForList(query.capture(), org.mockito.ArgumentMatchers.any(Object[].class)); + assertTrue(query.getValue().endsWith("VanID = ?"), query.getValue()); + } + } + + @Test + void checkRecordIsAlreadyPresentOrNot_matchesAStockRowOnItsFacility() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertEquals(1, repository.checkRecordIsAlreadyPresentOrNot("db_iemr", "t_indentissue", "1", "2", + "VanSerialNo", 5)); + + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(db.constructed().get(0)).queryForList(query.capture(), org.mockito.ArgumentMatchers.any(Object[].class)); + assertTrue(query.getValue().endsWith("SyncFacilityID = ?"), query.getValue()); + } + } + + @Test + void checkRecordIsAlreadyPresentOrNot_reportsARowThatIsNotOnTheServerYet() { + try (MockedConstruction db = databaseReturning(new ArrayList<>())) { + assertEquals(0, repository.checkRecordIsAlreadyPresentOrNot("db_iemr", "t_benvisitdetail", "1", "2", + "VanSerialNo", 0)); + } + } + + @Test + void syncDataToCentralDB_appliesAnInsertBatch() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertArrayEquals(new int[] { 1 }, repository.syncDataToCentralDB("db_iemr", "t_benvisitdetail", + "VanSerialNo", "INSERT INTO db_iemr.t_benvisitdetail(VanSerialNo) VALUES (?)", + new ArrayList<>(Collections.singletonList(new Object[] { 1 })))); + } + } + + @Test + void syncDataToCentralDB_appliesAnUpdateBatch() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertArrayEquals(new int[] { 1 }, + repository.syncDataToCentralDB("db_iemr", "t_benvisitdetail", "VanSerialNo,VanID", + "UPDATE db_iemr.t_benvisitdetail SET VanSerialNo = ?", + new ArrayList<>(Collections.singletonList(new Object[] { 1 })))); + } + } + + @Test + void syncDataToCentralDB_appliesAnUpdateBatchWithoutServerColumns() { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertArrayEquals(new int[] { 1 }, repository.syncDataToCentralDB("db_iemr", "t_benvisitdetail", null, + "UPDATE db_iemr.t_benvisitdetail SET VanSerialNo = ?", + new ArrayList<>(Collections.singletonList(new Object[] { 1 })))); + } + } + + @Test + void getMasterDataFromTable_readsEachMasterTypeWithAndWithoutALastDownloadDate() throws Exception { + Timestamp lastDownload = Timestamp.valueOf("2024-01-01 00:00:00"); + + try (MockedConstruction db = mockConstruction(JdbcTemplate.class, (mock, context) -> { + when(mock.queryForList(anyString())).thenReturn(oneRow()); + when(mock.queryForList(anyString(), org.mockito.ArgumentMatchers.any(Object[].class))) + .thenReturn(oneRow()); + })) { + for (String masterType : List.of("A", "V", "P")) { + assertEquals(1, repository + .getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", masterType, lastDownload, 1, 2) + .size()); + assertEquals(1, repository + .getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", masterType, null, 1, 2).size()); + } + } + } + + @Test + void getMasterDataFromTable_readsNothingWhenNoMasterTypeWasGiven() throws Exception { + try (MockedConstruction db = databaseReturning(oneRow())) { + assertTrue(repository.getMasterDataFromTable("db_iemr", "t_benvisitdetail", "VanSerialNo", null, null, 1, 2) + .isEmpty()); + } + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImplTest.java b/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImplTest.java new file mode 100644 index 00000000..91278055 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/dataSyncLayerCentral/GetDataFromVanAndSyncToDBImplTest.java @@ -0,0 +1,336 @@ +/* +* 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.mmu.service.dataSyncLayerCentral; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.iemr.mmu.data.syncActivity_syncLayer.SyncUploadDataDigester; + +class GetDataFromVanAndSyncToDBImplTest { + + @Mock + private DataSyncRepositoryCentral dataSyncRepositoryCentral; + + @InjectMocks + private GetDataFromVanAndSyncToDBImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + /** A one-row upload request for the given table. */ + private String request(String tableName, String rowJson) { + return "{\"schemaName\":\"db_iemr\",\"tableName\":\"" + tableName + "\"," + + "\"vanAutoIncColumnName\":\"VanSerialNo\",\"serverColumns\":\"VanSerialNo,VanID,BeneficiaryRegID\"," + + "\"syncedBy\":\"nurse\",\"facilityID\":5,\"syncData\":[" + rowJson + "]}"; + } + + private String row(String tableName) { + return "{\"tableName\":\"" + tableName + "\",\"VanSerialNo\":1,\"VanID\":2,\"BeneficiaryRegID\":3}"; + } + + private SyncUploadDataDigester digester(String tableName, List> syncData) { + SyncUploadDataDigester digester = new SyncUploadDataDigester(); + digester.setSchemaName("db_iemr"); + digester.setTableName(tableName); + digester.setVanAutoIncColumnName("VanSerialNo"); + digester.setServerColumns("VanSerialNo,VanID,BeneficiaryRegID"); + digester.setSyncedBy("nurse"); + digester.setSyncData(syncData); + return digester; + } + + @Nested + @DisplayName("syncing an uploaded batch") + class SyncBatch { + + @Test + void syncDataToServer_rejectsARequestWithoutATableName() throws Exception { + String request = "{\"schemaName\":\"db_iemr\",\"syncData\":[]}"; + + assertEquals("Error: Invalid sync request.", service.syncDataToServer(request, "auth")); + } + + @Test + void syncDataToServer_marksTheProvisionedBeneficiaryIdMappings() throws Exception { + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[] { 1 }); + + String request = "{\"schemaName\":\"db_iemr\",\"tableName\":\"m_beneficiaryregidmapping\"," + + "\"serverColumns\":\"SyncedBy\",\"syncedBy\":\"nurse\"," + + "\"syncData\":[{\"BenRegId\":1,\"BeneficiaryID\":2,\"VanID\":3}]}"; + + assertEquals("Sync successful for m_beneficiaryregidmapping.", service.syncDataToServer(request, "auth")); + } + + @Test + void syncDataToServer_reportsAPartiallyProvisionedBeneficiaryIdMapping() throws Exception { + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[0]); + + String request = "{\"schemaName\":\"db_iemr\",\"tableName\":\"m_beneficiaryregidmapping\"," + + "\"serverColumns\":\"SyncedBy\",\"syncedBy\":\"nurse\"," + + "\"syncData\":[{\"BenRegId\":1,\"BeneficiaryID\":2,\"VanID\":3}]}"; + + assertEquals("Sync failed for m_beneficiaryregidmapping.", service.syncDataToServer(request, "auth")); + } + + @Test + void syncDataToServer_skipsIncompleteBeneficiaryIdMappings() throws Exception { + String request = "{\"schemaName\":\"db_iemr\",\"tableName\":\"m_beneficiaryregidmapping\"," + + "\"serverColumns\":\"SyncedBy\",\"syncedBy\":\"nurse\",\"syncData\":[{\"BenRegId\":1}]}"; + + assertEquals("Sync successful for m_beneficiaryregidmapping.", service.syncDataToServer(request, "auth")); + verify(dataSyncRepositoryCentral, org.mockito.Mockito.never()).syncDataToCentralDB(anyString(), + anyString(), any(), anyString(), any()); + } + + @Test + void syncDataToServer_reportsAFailedBeneficiaryIdMappingUpdate() throws Exception { + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenThrow(new RuntimeException("db down")); + + String request = "{\"schemaName\":\"db_iemr\",\"tableName\":\"m_beneficiaryregidmapping\"," + + "\"serverColumns\":\"SyncedBy\",\"syncedBy\":\"nurse\"," + + "\"syncData\":[{\"BenRegId\":1,\"BeneficiaryID\":2,\"VanID\":3}]}"; + + assertEquals("Sync failed for m_beneficiaryregidmapping.", service.syncDataToServer(request, "auth")); + } + + @Test + void syncDataToServer_insertsARowThatIsNotOnTheServerYet() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(0); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[] { 1 }); + + String response = service.syncDataToServer(request("t_benvisitdetail", row("t_benvisitdetail")), "auth"); + + assertTrue(response.contains("\"success\":true"), response); + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(dataSyncRepositoryCentral).syncDataToCentralDB(anyString(), anyString(), any(), query.capture(), + any()); + assertTrue(query.getValue().startsWith("INSERT INTO db_iemr.t_benvisitdetail"), query.getValue()); + } + + @Test + void syncDataToServer_updatesARowThatIsAlreadyOnTheServer() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(1); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[] { 1 }); + + String response = service.syncDataToServer(request("t_benvisitdetail", row("t_benvisitdetail")), "auth"); + + assertTrue(response.contains("\"success\":true"), response); + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(dataSyncRepositoryCentral).syncDataToCentralDB(anyString(), anyString(), any(), query.capture(), + any()); + assertTrue(query.getValue().contains("AND VanID = ?"), query.getValue()); + } + + @Test + void syncDataToServer_reportsARowTheServerRejected() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(0); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[] { 0 }); + + String response = service.syncDataToServer(request("t_benvisitdetail", row("t_benvisitdetail")), "auth"); + + assertTrue(response.contains("Insert failed"), response); + } + + @Test + void syncDataToServer_reportsARowThatMatchedNothingOnUpdate() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(1); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[] { 0 }); + + assertTrue(service.syncDataToServer(request("t_benvisitdetail", row("t_benvisitdetail")), "auth") + .contains("No matching row")); + } + + @Test + void syncDataToServer_reportsAnInsertThatFailedForTheWholeBatch() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(0); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenThrow(new RuntimeException("Duplicate entry '1' for key 'PRIMARY'")); + + assertTrue(service.syncDataToServer(request("t_benvisitdetail", row("t_benvisitdetail")), "auth") + .contains("INSERT: Duplicate key: PRIMARY")); + } + + @Test + void syncDataToServer_reportsAnUpdateThatFailedForTheWholeBatch() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(1); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenThrow(new RuntimeException("Database connection timeout")); + + assertTrue(service.syncDataToServer(request("t_benvisitdetail", row("t_benvisitdetail")), "auth") + .contains("UPDATE: Database connection timeout")); + } + + @Test + void syncDataToServer_reportsARowWhoseExistenceCouldNotBeChecked() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenThrow(new RuntimeException("db down")); + + assertTrue(service.syncDataToServer(request("t_benvisitdetail", row("t_benvisitdetail")), "auth") + .contains("Record check failed")); + } + + @Test + void syncDataToServer_fallsBackToAGenericSyncForATableOutsideTheKnownGroups() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(0); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[] { 1 }); + + assertTrue(service.syncDataToServer(request("t_unknown_table", row("t_other_table")), "auth") + .contains("Data sync completed")); + } + + @Test + void syncDataToServer_readsTheDateFormattedColumnNamesTheVanSends() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(0); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[] { 1 }); + + String rowWithFormattedDate = "{\"tableName\":\"t_benvisitdetail\",\"VanSerialNo\":1,\"VanID\":2," + + "\"date_format(BeneficiaryRegID,'%Y')\":3}"; + + assertTrue(service.syncDataToServer(request("t_benvisitdetail", rowWithFormattedDate), "auth") + .contains("\"success\":true")); + } + + @Test + void syncDataToServer_marksAStockRowAsProcessedForItsOwnFacility() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(1); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[] { 1 }); + + String stockRow = "{\"tableName\":\"t_itemstockentry\",\"VanSerialNo\":1,\"VanID\":2," + + "\"FacilityID\":5,\"SyncFacilityID\":5}"; + + assertTrue(service.syncDataToServer(request("t_itemstockentry", stockRow), "auth") + .contains("\"success\":true")); + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + verify(dataSyncRepositoryCentral).syncDataToCentralDB(anyString(), anyString(), any(), query.capture(), + any()); + assertTrue(query.getValue().contains("AND SyncFacilityID = ?"), query.getValue()); + } + + @Test + void syncDataToServer_marksAnIndentRowAsProcessedForItsOwnFacility() throws Exception { + when(dataSyncRepositoryCentral.checkRecordIsAlreadyPresentOrNot(anyString(), anyString(), anyString(), + anyString(), anyString(), anyInt())).thenReturn(0); + when(dataSyncRepositoryCentral.syncDataToCentralDB(anyString(), anyString(), any(), anyString(), any())) + .thenReturn(new int[] { 1 }); + + for (String tableAndKey : List.of("t_indent:FromFacilityID", "t_indentorder:FromFacilityID", + "t_indentissue:ToFacilityID", "t_stocktransfer:TransferToFacilityID")) { + String table = tableAndKey.split(":")[0]; + String key = tableAndKey.split(":")[1]; + String stockRow = "{\"tableName\":\"" + table + "\",\"VanSerialNo\":1,\"VanID\":2,\"" + key + + "\":5}"; + + assertTrue(service.syncDataToServer(request(table, stockRow), "auth").contains("Data sync completed")); + } + } + } + + @Nested + @DisplayName("sync queries and helpers") + class QueriesAndHelpers { + + @Test + void getQueryToUpdateDataToServerDB_matchesOnTheVanForAClinicalTable() { + String query = service.getQueryToUpdateDataToServerDB("db_iemr", "VanSerialNo,VanID", "t_benvisitdetail"); + + assertTrue(query.startsWith("UPDATE db_iemr.t_benvisitdetail SET"), query); + assertTrue(query.contains("VanSerialNo = ?, VanID = ?"), query); + assertTrue(query.contains("AND VanID = ?"), query); + } + + @Test + void getQueryToUpdateDataToServerDB_matchesOnTheFacilityForAStockTable() { + assertTrue(service.getQueryToUpdateDataToServerDB("db_iemr", "VanSerialNo", "t_patientissue") + .contains("AND SyncFacilityID = ?")); + } + + @Test + void getQueryToUpdateDataToServerDB_toleratesATableWithoutColumns() { + assertTrue(service.getQueryToUpdateDataToServerDB("db_iemr", null, "t_benvisitdetail") + .contains("UPDATE db_iemr.t_benvisitdetail SET WHERE VanSerialNo = ?")); + } + + @Test + void update_I_BeneficiaryDetails_for_processed_in_batches_reportsTheBatchOutcome() { + when(dataSyncRepositoryCentral.getBatchForBenDetails(any(), anyString(), anyInt(), anyInt())) + .thenReturn(new ArrayList<>()); + + assertEquals("data sync passed", + service.update_I_BeneficiaryDetails_for_processed_in_batches( + digester("i_beneficiarydetails", new ArrayList<>()))); + } + + @Test + void update_I_BeneficiaryDetails_for_processed_in_batches_reportsAFailedRead() { + when(dataSyncRepositoryCentral.getBatchForBenDetails(any(), anyString(), anyInt(), anyInt())) + .thenThrow(new RuntimeException("db down")); + + assertTrue(service + .update_I_BeneficiaryDetails_for_processed_in_batches( + digester("i_beneficiarydetails", new ArrayList<>())) + .contains("Error fetching data")); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/health/HealthServiceTest.java b/src/test/java/com/iemr/mmu/service/health/HealthServiceTest.java new file mode 100644 index 00000000..8d0ce598 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/health/HealthServiceTest.java @@ -0,0 +1,353 @@ +/* +* 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.mmu.service.health; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.util.ReflectionTestUtils; + +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.HikariPoolMXBean; + +class HealthServiceTest { + + /** A data source whose health query returns a row, and whose diagnostic queries are quiet. */ + private DataSource healthyDataSource(int lockWaits, int slowQueries) throws SQLException { + DataSource dataSource = mock(DataSource.class); + when(dataSource.getConnection()).thenAnswer(invocation -> connection(lockWaits, slowQueries)); + return dataSource; + } + + private Connection connection(int lockWaits, int slowQueries) throws SQLException { + Connection connection = mock(Connection.class); + when(connection.prepareStatement(anyString())).thenAnswer(invocation -> { + String sql = invocation.getArgument(0); + if (sql.contains("health_check")) { + return statementReturning(1); + } + if (sql.contains("metadata lock")) { + return statementReturning(lockWaits); + } + return statementReturning(slowQueries); + }); + return connection; + } + + private PreparedStatement statementReturning(int count) throws SQLException { + PreparedStatement statement = mock(PreparedStatement.class); + ResultSet resultSet = mock(ResultSet.class); + when(resultSet.next()).thenReturn(true); + when(resultSet.getInt(1)).thenReturn(count); + when(statement.executeQuery()).thenReturn(resultSet); + return statement; + } + + @SuppressWarnings("unchecked") + private RedisTemplate redisAnswering(String pong) { + RedisTemplate redisTemplate = mock(RedisTemplate.class); + when(redisTemplate.execute(any(RedisCallback.class))).thenAnswer(invocation -> { + RedisConnection connection = mock(RedisConnection.class); + when(connection.ping()).thenReturn(pong); + return ((RedisCallback) invocation.getArgument(0)).doInRedis(connection); + }); + return redisTemplate; + } + + @SuppressWarnings("unchecked") + private Map componentOf(Map health, String name) { + return ((Map>) health.get("components")).get(name); + } + + @Test + @DisplayName("both components report UP when the database and Redis answer") + void checkHealth_reportsEverythingUpWhenBothComponentsAnswer() throws Exception { + HealthService service = new HealthService(healthyDataSource(0, 0), redisAnswering("PONG")); + try { + Map health = service.checkHealth(); + + assertEquals("UP", health.get("status")); + assertEquals("UP", componentOf(health, "mysql").get("status")); + assertEquals("UP", componentOf(health, "redis").get("status")); + assertNotNull(health.get("timestamp")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("MySQL reports DOWN when the connection cannot be opened") + void checkHealth_reportsMysqlDownWhenTheConnectionFails() throws Exception { + DataSource dataSource = mock(DataSource.class); + when(dataSource.getConnection()).thenThrow(new SQLException("connection refused")); + + HealthService service = new HealthService(dataSource, redisAnswering("PONG")); + try { + Map health = service.checkHealth(); + + assertEquals("DOWN", health.get("status")); + assertEquals("DOWN", componentOf(health, "mysql").get("status")); + assertEquals("CRITICAL", componentOf(health, "mysql").get("severity")); + assertEquals("MySQL connection failed", componentOf(health, "mysql").get("error")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("MySQL reports DOWN when the health query returns no row") + void checkHealth_reportsMysqlDownWhenTheHealthQueryReturnsNothing() throws Exception { + Connection connection = mock(Connection.class); + PreparedStatement statement = mock(PreparedStatement.class); + ResultSet resultSet = mock(ResultSet.class); + when(resultSet.next()).thenReturn(false); + when(statement.executeQuery()).thenReturn(resultSet); + when(connection.prepareStatement(anyString())).thenReturn(statement); + DataSource dataSource = mock(DataSource.class); + when(dataSource.getConnection()).thenReturn(connection); + + HealthService service = new HealthService(dataSource, redisAnswering("PONG")); + try { + Map health = service.checkHealth(); + + assertEquals("No result from health check query", componentOf(health, "mysql").get("error")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("MySQL reports DEGRADED when the database is waiting on locks") + void checkHealth_reportsMysqlDegradedWhenTheDatabaseIsWaitingOnLocks() throws Exception { + HealthService service = new HealthService(healthyDataSource(2, 0), redisAnswering("PONG")); + try { + Map health = service.checkHealth(); + + assertEquals("DEGRADED", health.get("status")); + assertEquals("DEGRADED", componentOf(health, "mysql").get("status")); + assertEquals("WARNING", componentOf(health, "mysql").get("severity")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("MySQL reports DEGRADED when too many slow queries are running") + void checkHealth_reportsMysqlDegradedWhenTooManySlowQueriesAreRunning() throws Exception { + HealthService service = new HealthService(healthyDataSource(0, 5), redisAnswering("PONG")); + try { + assertEquals("DEGRADED", service.checkHealth().get("status")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("the advanced diagnostics are only re-run once the throttle window passes") + void checkHealth_reusesTheCachedDiagnosticsWithinTheThrottleWindow() throws Exception { + HealthService service = new HealthService(healthyDataSource(2, 0), redisAnswering("PONG")); + try { + service.checkHealth(); + + assertEquals("DEGRADED", service.checkHealth().get("status")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("Redis reports DOWN when the server does not answer PING") + void checkHealth_reportsRedisDownWhenPingIsNotAnswered() throws Exception { + HealthService service = new HealthService(healthyDataSource(0, 0), redisAnswering("NOPE")); + try { + Map health = service.checkHealth(); + + assertEquals("DOWN", health.get("status")); + assertEquals("Redis PING failed", componentOf(health, "redis").get("error")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("Redis reports DOWN when the connection fails") + @SuppressWarnings("unchecked") + void checkHealth_reportsRedisDownWhenTheConnectionFails() throws Exception { + RedisTemplate redisTemplate = mock(RedisTemplate.class); + when(redisTemplate.execute(any(RedisCallback.class))).thenThrow(new RuntimeException("connection refused")); + + HealthService service = new HealthService(healthyDataSource(0, 0), redisTemplate); + try { + assertEquals("Redis connection failed", componentOf(service.checkHealth(), "redis").get("error")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("Redis is skipped, and stays UP, when it is not configured") + void checkHealth_skipsRedisWhenItIsNotConfigured() throws Exception { + HealthService service = new HealthService(healthyDataSource(0, 0), null); + try { + Map health = service.checkHealth(); + + assertEquals("UP", health.get("status")); + assertEquals("Redis not configured — skipped", componentOf(health, "redis").get("message")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("a component that never ran is reported as DOWN") + void checkHealth_reportsAComponentThatNeverRanAsDown() throws Exception { + HealthService service = new HealthService(healthyDataSource(0, 0), redisAnswering("PONG")); + service.shutdown(); + + Map health = service.checkHealth(); + + assertEquals("DOWN", health.get("status")); + assertTrue(((String) componentOf(health, "mysql").get("error")).contains("did not complete in time")); + assertTrue(((String) componentOf(health, "redis").get("error")).contains("did not complete in time")); + } + + @Test + @DisplayName("a pool that is nearly exhausted degrades the database") + void checkHealth_reportsMysqlDegradedWhenTheConnectionPoolIsNearlyExhausted() throws Exception { + HikariDataSource dataSource = mock(HikariDataSource.class); + when(dataSource.getConnection()).thenAnswer(invocation -> connection(0, 0)); + HikariPoolMXBean poolMXBean = mock(HikariPoolMXBean.class); + when(poolMXBean.getActiveConnections()).thenReturn(9); + when(dataSource.getHikariPoolMXBean()).thenReturn(poolMXBean); + when(dataSource.getMaximumPoolSize()).thenReturn(10); + + HealthService service = new HealthService(dataSource, redisAnswering("PONG")); + try { + assertEquals("DEGRADED", service.checkHealth().get("status")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("a pool with headroom leaves the database healthy") + void checkHealth_leavesMysqlHealthyWhenTheConnectionPoolHasHeadroom() throws Exception { + HikariDataSource dataSource = mock(HikariDataSource.class); + when(dataSource.getConnection()).thenAnswer(invocation -> connection(0, 0)); + HikariPoolMXBean poolMXBean = mock(HikariPoolMXBean.class); + when(poolMXBean.getActiveConnections()).thenReturn(1); + when(dataSource.getHikariPoolMXBean()).thenReturn(poolMXBean); + when(dataSource.getMaximumPoolSize()).thenReturn(10); + + HealthService service = new HealthService(dataSource, redisAnswering("PONG")); + try { + assertEquals("UP", service.checkHealth().get("status")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("unreadable pool metrics leave the database healthy") + void checkHealth_leavesMysqlHealthyWhenThePoolMetricsCannotBeRead() throws Exception { + HikariDataSource dataSource = mock(HikariDataSource.class); + when(dataSource.getConnection()).thenAnswer(invocation -> connection(0, 0)); + when(dataSource.getHikariPoolMXBean()).thenThrow(new IllegalStateException("pool not started")); + + HealthService service = new HealthService(dataSource, redisAnswering("PONG")); + try { + assertEquals("UP", service.checkHealth().get("status")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("a database that fails mid-diagnosis is reported as degraded") + void checkHealth_reportsMysqlDegradedWhenTheDiagnosticQueriesFail() throws Exception { + DataSource dataSource = mock(DataSource.class); + when(dataSource.getConnection()).thenAnswer(new org.mockito.stubbing.Answer() { + private int call = 0; + + @Override + public Connection answer(org.mockito.invocation.InvocationOnMock invocation) throws Throwable { + // The first connection serves the basic health query; the diagnostics that + // follow cannot get one. + if (call++ == 0) { + return connection(0, 0); + } + throw new SQLException("pool exhausted"); + } + }); + + HealthService service = new HealthService(dataSource, redisAnswering("PONG")); + try { + assertEquals("DEGRADED", service.checkHealth().get("status")); + } finally { + service.shutdown(); + } + } + + @Test + @DisplayName("shutdown is safe to call twice") + void shutdown_isSafeToCallTwice() throws Exception { + HealthService service = new HealthService(healthyDataSource(0, 0), redisAnswering("PONG")); + + service.shutdown(); + service.shutdown(); + } + + @Test + @DisplayName("a slow component is flagged with a warning severity") + void checkHealth_flagsASlowComponentWithAWarningSeverity() throws Exception { + HealthService service = new HealthService(healthyDataSource(0, 0), redisAnswering("PONG")); + try { + String severity = (String) ReflectionTestUtils.invokeMethod(service, "determineSeverity", true, 5000L, + false); + assertEquals("WARNING", severity); + assertEquals("OK", ReflectionTestUtils.invokeMethod(service, "determineSeverity", true, 10L, false)); + assertEquals("CRITICAL", ReflectionTestUtils.invokeMethod(service, "determineSeverity", false, 10L, + false)); + } finally { + service.shutdown(); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/ncdCare/NCDCareServiceImplTest.java b/src/test/java/com/iemr/mmu/service/ncdCare/NCDCareServiceImplTest.java new file mode 100644 index 00000000..605aef47 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/ncdCare/NCDCareServiceImplTest.java @@ -0,0 +1,534 @@ +/* +* 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.mmu.service.ncdCare; + +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.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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus; +import com.iemr.mmu.data.nurse.CommonUtilityClass; +import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo; +import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl; +import com.iemr.mmu.service.labtechnician.LabTechnicianServiceImpl; +import com.iemr.mmu.service.tele_consultation.TeleConsultationServiceImpl; + +class NCDCareServiceImplTest { + + @Mock + private CommonNurseServiceImpl commonNurseServiceImpl; + @Mock + private CommonDoctorServiceImpl commonDoctorServiceImpl; + @Mock + private NCDCareDoctorServiceImpl ncdCareDoctorServiceImpl; + @Mock + private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl; + @Mock + private LabTechnicianServiceImpl labTechnicianServiceImpl; + @Mock + private TeleConsultationServiceImpl teleConsultationServiceImpl; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + + @InjectMocks + private NCDCareServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + /** The visit block every nurse save starts from, with no test ordered. */ + private static String visitDetailsBlock(String laboratoryList) { + return "\"visitDetails\":{\"visitDetails\":{\"beneficiaryRegID\":1,\"visitReason\":\"Follow Up\"," + + "\"visitCategory\":\"NCD care\"},\"adherence\":{}," + + "\"investigation\":{\"laboratoryList\":" + laboratoryList + "}}"; + } + + @Nested + @DisplayName("saving nurse data") + class NurseSave { + + @Test + void saveNCDCareNurseData_ignoresARequestWithoutVisitDetails() throws Exception { + assertNull(service.saveNCDCareNurseData(null)); + assertNull(service.saveNCDCareNurseData(json("{}"))); + } + + @Test + void saveNCDCareNurseData_skipsTheVisitWhenTheNurseAlreadySavedThisFlow() throws Exception { + when(beneficiaryFlowStatusRepo.checkExistData(any(), any())).thenReturn(new BeneficiaryFlowStatus()); + + assertEquals(0L, service.saveNCDCareNurseData(json("{" + visitDetailsBlock("[]") + "}"))); + verify(commonNurseServiceImpl, never()).saveBeneficiaryVisitDetails(any()); + } + + @Test + void saveNCDCareNurseData_returnsZeroWhenTheVisitWasNotCreated() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1); + + assertEquals(0L, service.saveNCDCareNurseData(json("{" + visitDetailsBlock("[]") + "}"))); + } + + @Test + void saveNCDCareNurseData_sendsTheBeneficiaryStraightToTheDoctorWhenNoTestWasOrdered() throws Exception { + stubVisitCreation(); + stubSuccessfulSections(); + + String request = "{" + visitDetailsBlock("[]") + ",\"historyDetails\":{},\"vitalDetails\":{}}"; + + assertEquals(1L, service.saveNCDCareNurseData(json(request))); + verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), org.mockito.ArgumentMatchers.eq((short) 9), + org.mockito.ArgumentMatchers.eq((short) 1), org.mockito.ArgumentMatchers.eq((short) 0), any(), + any(), anyLong(), any()); + } + + @Test + void saveNCDCareNurseData_routesTheBeneficiaryThroughTheLabWhenATestWasOrdered() throws Exception { + stubVisitCreation(); + stubSuccessfulSections(); + when(commonNurseServiceImpl.saveBenInvestigationDetails(any())).thenReturn(1); + + String request = "{" + visitDetailsBlock("[{\"testID\":1}]") + + ",\"historyDetails\":{},\"vitalDetails\":{}}"; + + assertEquals(1L, service.saveNCDCareNurseData(json(request))); + verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), org.mockito.ArgumentMatchers.eq((short) 2), + org.mockito.ArgumentMatchers.eq((short) 0), org.mockito.ArgumentMatchers.eq((short) 1), any(), + any(), anyLong(), any()); + } + + @Test + void saveNCDCareNurseData_leavesTheFlowUntouchedWhenTheVitalsFailToSave() throws Exception { + stubVisitCreation(); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(null); + + String request = "{" + visitDetailsBlock("[]") + ",\"historyDetails\":{},\"vitalDetails\":{}}"; + + assertNull(service.saveNCDCareNurseData(json(request))); + verify(commonBenStatusFlowServiceImpl, never()).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), + anyLong(), anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any()); + } + + private void stubVisitCreation() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + } + + private void stubSuccessfulSections() { + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + } + + @Test + void saveBenVisitDetails_savesAdherenceAndInvestigationsAgainstTheNewVisit() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + when(commonNurseServiceImpl.saveBenAdherenceDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigationDetails(any())).thenReturn(1); + + Map result = service.saveBenVisitDetails( + json("{" + visitDetailsBlock("[{\"testID\":1}]") + "}").getAsJsonObject("visitDetails"), + new CommonUtilityClass()); + + assertEquals(5L, result.get("visitID")); + assertEquals(6L, result.get("visitCode")); + } + + @Test + void saveBenVisitDetails_returnsNothingWhenTheVisitBlockIsMissing() throws Exception { + assertTrue(service.saveBenVisitDetails(json("{}"), new CommonUtilityClass()).isEmpty()); + assertTrue(service.saveBenVisitDetails(null, new CommonUtilityClass()).isEmpty()); + } + } + + @Nested + @DisplayName("saving the individual nurse sections") + class NurseSections { + + @Test + void saveBenNCDCareHistoryDetails_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenNCDCareHistoryDetails(json("{}"), 1L, 2L)); + assertEquals(1L, service.saveBenNCDCareHistoryDetails(null, 1L, 2L)); + } + + @Test + void saveBenNCDCareHistoryDetails_savesEverySectionThatWasSent() throws Exception { + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenComorbidConditions(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenMedicationHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveFemaleObstetricHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenMenstrualHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenFamilyHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePersonalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.saveAllergyHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildOptionalVaccineDetail(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveImmunizationHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildDevelopmentHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildFeedingHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePerinatalHistory(any())).thenReturn(1L); + + String history = "{\"pastHistory\":{},\"comorbidConditions\":{}," + + "\"medicationHistory\":{\"medicationHistoryList\":[{\"currentMedication\":\"x\"}]}," + + "\"femaleObstetricHistory\":{},\"menstrualHistory\":{},\"familyHistory\":{}," + + "\"personalHistory\":{},\"childVaccineDetails\":{},\"immunizationHistory\":{}," + + "\"developmentHistory\":{},\"feedingHistory\":{},\"perinatalHistroy\":{}}"; + + assertEquals(1L, service.saveBenNCDCareHistoryDetails(json(history), 1L, 2L)); + } + + @Test + void saveBenNCDCareHistoryDetails_reportsFailureWhenASectionCouldNotBeSaved() throws Exception { + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(0L); + assertNull(service.saveBenNCDCareHistoryDetails(json("{\"pastHistory\":{}}"), 1L, 2L)); + } + + @Test + void saveBenNCDCareVitalDetails_savesAnthropometryAndVitalsTogether() throws Exception { + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(5L); + + assertEquals(4L, service.saveBenNCDCareVitalDetails(json("{}"), 1L, 2L)); + } + + @Test + void saveBenNCDCareVitalDetails_reportsFailureWhenTheVitalsCouldNotBeSaved() throws Exception { + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(null); + + assertNull(service.saveBenNCDCareVitalDetails(json("{}"), 1L, 2L)); + assertNull(service.saveBenNCDCareVitalDetails(null, 1L, 2L)); + } + } + + @Nested + @DisplayName("reading the nurse and doctor case sheets") + class Reads { + + @Test + void getBenVisitDetailsFrmNurseNCDCare_gathersTheVisitAdherenceAndInvestigations() throws Exception { + when(commonNurseServiceImpl.getCSVisitDetails(1L, 2L)).thenReturn(null); + when(commonNurseServiceImpl.getBenAdherence(1L, 2L)).thenReturn("adherence"); + when(commonNurseServiceImpl.getLabTestOrders(1L, 2L)).thenReturn("orders"); + + String result = service.getBenVisitDetailsFrmNurseNCDCare(1L, 2L); + + assertTrue(result.contains("NCDCareNurseVisitDetail")); + assertTrue(result.contains("BenAdherence")); + assertTrue(result.contains("Investigation")); + } + + @Test + void getBenNCDCareHistoryDetails_gathersEveryHistorySection() { + when(commonNurseServiceImpl.getPastHistoryData(1L, 2L)) + .thenReturn(new com.iemr.mmu.data.anc.BenMedHistory()); + + assertTrue(service.getBenNCDCareHistoryDetails(1L, 2L).contains("PastHistory")); + verify(commonNurseServiceImpl).getFeedingHistory(1L, 2L); + } + + @Test + void getBeneficiaryVitalDetails_gathersAnthropometryAndVitals() { + when(commonNurseServiceImpl.getBeneficiaryPhysicalAnthropometryDetails(1L, 2L)).thenReturn("a"); + when(commonNurseServiceImpl.getBeneficiaryPhysicalVitalDetails(1L, 2L)).thenReturn("v"); + + assertTrue(service.getBeneficiaryVitalDetails(1L, 2L).contains("benAnthropometryDetail")); + } + + @Test + void getBenNCDCareNurseData_combinesTheVitalsAndHistory() { + String result = service.getBenNCDCareNurseData(1L, 2L); + assertTrue(result.contains("vitals")); + assertTrue(result.contains("history")); + } + + @Test + void getBenCaseRecordFromDoctorNCDCare_gathersEveryDoctorSection() throws Exception { + when(commonDoctorServiceImpl.getFindingsDetails(1L, 2L)).thenReturn("findings"); + when(ncdCareDoctorServiceImpl.getNCDCareDiagnosisDetails(1L, 2L)).thenReturn("diagnosis"); + when(commonDoctorServiceImpl.getInvestigationDetails(1L, 2L)).thenReturn("investigation"); + when(commonDoctorServiceImpl.getPrescribedDrugs(1L, 2L)).thenReturn("prescription"); + when(commonDoctorServiceImpl.getReferralDetails(1L, 2L)).thenReturn("refer"); + when(labTechnicianServiceImpl.getLabResultDataForBen(1L, 2L)).thenReturn(new ArrayList<>()); + when(commonNurseServiceImpl.getGraphicalTrendData(1L, "ncdCare")).thenReturn(new HashMap<>()); + when(labTechnicianServiceImpl.getLast_3_ArchivedTestVisitList(1L, 2L)).thenReturn("[]"); + + String result = service.getBenCaseRecordFromDoctorNCDCare(1L, 2L); + + assertTrue(result.contains("findings")); + assertTrue(result.contains("GraphData")); + } + } + + @Nested + @DisplayName("updating nurse data") + class NurseUpdates { + + @Test + void updateBenHistoryDetails_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1, service.updateBenHistoryDetails(json("{}"))); + } + + @Test + void updateBenHistoryDetails_updatesEverySectionThatWasSent() throws Exception { + when(commonNurseServiceImpl.updateBenPastHistoryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenComorbidConditions(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenMedicationHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenPersonalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenAllergicHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenFamilyHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateMenstrualHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePastObstetricHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildImmunizationDetail(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildOptionalVaccineDetail(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildFeedingHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePerinatalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildDevelopmentHistory(any())).thenReturn(1); + + String history = "{\"pastHistory\":{},\"comorbidConditions\":{},\"medicationHistory\":{}," + + "\"personalHistory\":{},\"familyHistory\":{},\"menstrualHistory\":{}," + + "\"femaleObstetricHistory\":{},\"immunizationHistory\":{\"benChildVaccineDetails\":[{}]}," + + "\"childVaccineDetails\":{},\"feedingHistory\":{},\"perinatalHistroy\":{}," + + "\"developmentHistory\":{}}"; + + assertEquals(1, service.updateBenHistoryDetails(json(history))); + } + + @Test + void updateBenHistoryDetails_reportsFailureWhenASectionCouldNotBeUpdated() throws Exception { + when(commonNurseServiceImpl.updateBenPastHistoryDetails(any())).thenReturn(0); + assertEquals(0, service.updateBenHistoryDetails(json("{\"pastHistory\":{}}"))); + } + + @Test + void updateBenVitalDetails_updatesAnthropometryAndVitalsTogether() throws Exception { + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(1); + + assertEquals(1, service.updateBenVitalDetails(json("{}"))); + } + + @Test + void updateBenVitalDetails_reportsFailureWhenTheVitalsCouldNotBeUpdated() throws Exception { + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(0); + + assertEquals(0, service.updateBenVitalDetails(json("{}"))); + assertEquals(1, service.updateBenVitalDetails(null)); + } + } + + @Nested + @DisplayName("saving and updating doctor data") + class DoctorData { + + private String doctorRequest(String extra) { + return "{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"providerServiceMapID\":4," + + "\"createdBy\":\"doctor\",\"findings\":{},\"investigation\":{\"laboratoryList\":[{}]}," + + "\"diagnosis\":{\"provisionalDiagnosisList\":[{\"term\":\"Fever\",\"conceptID\":\"1\"}]}," + + "\"prescription\":[{\"drugID\":1}],\"refer\":{}" + extra + "}"; + } + + private String teleconsultationBlock() { + return ",\"serviceID\":4,\"tcRequest\":{\"userID\":5,\"allocationDate\":\"2024-01-01\"," + + "\"fromTime\":\"10:00:00\",\"toTime\":\"10:30:00\"}"; + } + + private Map drugResult() { + Map result = new HashMap<>(); + result.put("count", 1); + result.put("prescribedDrugIDs", Collections.singletonList(9L)); + return result; + } + + @Test + void saveDoctorData_savesEverySectionAndAdvancesTheFlow() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.savePrescriptionDetailsAndGetPrescriptionID(any(), any(), any(), any(), any(), + any(), any(), any(), any())).thenReturn(7L); + when(ncdCareDoctorServiceImpl.saveNCDDiagnosisData(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.saveDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void saveDoctorData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.savePrescriptionDetailsAndGetPrescriptionID(any(), any(), any(), any(), any(), + any(), any(), any(), any())).thenReturn(7L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.saveDoctorData(json("{\"findings\":{},\"investigation\":{}}"), "auth")); + } + + @Test + void saveDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.savePrescriptionDetailsAndGetPrescriptionID(any(), any(), any(), any(), any(), + any(), any(), any(), any())).thenReturn(7L); + when(ncdCareDoctorServiceImpl.saveNCDDiagnosisData(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, () -> service.saveDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void saveDoctorData_failsWhenASectionCouldNotBeSaved() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(0); + when(commonNurseServiceImpl.savePrescriptionDetailsAndGetPrescriptionID(any(), any(), any(), any(), any(), + any(), any(), any(), any())).thenReturn(7L); + + assertThrows(RuntimeException.class, () -> service.saveDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void saveDoctorData_booksTheSpecialistSlotBeforeRaisingATeleconsultationRequest() throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(1); + when(teleConsultationServiceImpl.createTCRequest(any())).thenReturn(1); + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.savePrescriptionDetailsAndGetPrescriptionID(any(), any(), any(), any(), any(), + any(), any(), any(), any())).thenReturn(7L); + when(ncdCareDoctorServiceImpl.saveNCDDiagnosisData(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.saveDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + verify(teleConsultationServiceImpl).createTCRequest(any()); + } + + @Test + void saveDoctorData_failsWhenTheSpecialistSlotCouldNotBeBooked() throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(0); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.saveDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + assertEquals("Error while booking slot.", thrown.getMessage()); + } + + @Test + void updateNCDCareDoctorData_updatesEverySectionAndAdvancesTheFlow() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(ncdCareDoctorServiceImpl.updateBenNCDCareDiagnosis(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.updateNCDCareDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void updateNCDCareDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(ncdCareDoctorServiceImpl.updateBenNCDCareDiagnosis(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, + () -> service.updateNCDCareDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void updateNCDCareDoctorData_failsWhenASectionCouldNotBeUpdated() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(0); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + + assertThrows(RuntimeException.class, + () -> service.updateNCDCareDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void updateNCDCareDoctorData_booksTheSpecialistSlotBeforeRaisingATeleconsultationRequest() throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(1); + when(teleConsultationServiceImpl.createTCRequest(any())).thenReturn(1); + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(ncdCareDoctorServiceImpl.updateBenNCDCareDiagnosis(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.updateNCDCareDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + } + + @Test + void updateNCDCareDoctorData_failsWhenTheSpecialistSlotCouldNotBeBooked() throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(0); + + assertThrows(RuntimeException.class, + () -> service.updateNCDCareDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/ncdscreening/NCDScreeningNurseAndDoctorServiceTest.java b/src/test/java/com/iemr/mmu/service/ncdscreening/NCDScreeningNurseAndDoctorServiceTest.java new file mode 100644 index 00000000..bb473e82 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/ncdscreening/NCDScreeningNurseAndDoctorServiceTest.java @@ -0,0 +1,269 @@ +/* +* 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.mmu.service.ncdscreening; + +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.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.ncdScreening.NCDScreening; +import com.iemr.mmu.data.quickConsultation.PrescriptionDetail; +import com.iemr.mmu.repo.nurse.ncdscreening.NCDScreeningRepo; +import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo; +import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl; +import com.iemr.mmu.service.tele_consultation.TeleConsultationServiceImpl; + +class NCDScreeningNurseAndDoctorServiceTest { + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + @Nested + @DisplayName("NCDScreeningNurseServiceImpl") + class NurseService { + + @Mock + private NCDScreeningRepo ncdScreeningRepo; + + @InjectMocks + private NCDScreeningNurseServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private NCDScreening screeningWithConditions() { + NCDScreening screening = new NCDScreening(); + ArrayList> conditions = new ArrayList<>(); + for (int i = 1; i <= 2; i++) { + Map condition = new HashMap<>(); + condition.put("ncdScreeningConditionID", i); + condition.put("screeningCondition", "condition" + i); + conditions.add(condition); + } + screening.setNcdScreeningConditionList(conditions); + return screening; + } + + @Test + void saveNCDScreeningDetails_flattensTheScreenedConditionsBeforeSaving() { + NCDScreening screening = screeningWithConditions(); + NCDScreening stored = new NCDScreening(); + stored.setID(5L); + when(ncdScreeningRepo.save(screening)).thenReturn(stored); + + assertEquals(5L, service.saveNCDScreeningDetails(screening)); + assertEquals("1,2", screening.getNcdScreeningConditionID()); + assertEquals("condition1,condition2", screening.getScreeningCondition()); + } + + @Test + void saveNCDScreeningDetails_returnsNothingWhenTheRowWasNotStored() { + NCDScreening screening = new NCDScreening(); + when(ncdScreeningRepo.save(screening)).thenReturn(null); + + assertNull(service.saveNCDScreeningDetails(screening)); + } + + @Test + void getNCDScreeningDetails_splitsTheStoredConditionsBackIntoAList() { + NCDScreening stored = new NCDScreening(); + stored.setNcdScreeningConditionID("1,2"); + stored.setScreeningCondition("condition1,condition2"); + stored.setNextScreeningDateDB(Timestamp.valueOf("2024-06-01 00:00:00")); + when(ncdScreeningRepo.getNCDScreeningDetails(1L, 2L)).thenReturn(stored); + + String result = service.getNCDScreeningDetails(1L, 2L); + + assertEquals(2, stored.getNcdScreeningConditionList().size()); + assertTrue(result.contains("2024-06-01"), result); + } + + @Test + void getNCDScreeningDetails_leavesTheConditionListUnsetWhenNothingWasScreened() { + NCDScreening stored = new NCDScreening(); + when(ncdScreeningRepo.getNCDScreeningDetails(1L, 2L)).thenReturn(stored); + + service.getNCDScreeningDetails(1L, 2L); + + assertNull(stored.getNcdScreeningConditionList()); + } + + @Test + void getNCDScreeningDetails_ignoresConditionIdsAndNamesThatDoNotLineUp() { + NCDScreening stored = new NCDScreening(); + stored.setNcdScreeningConditionID("1,2"); + stored.setScreeningCondition("condition1"); + when(ncdScreeningRepo.getNCDScreeningDetails(1L, 2L)).thenReturn(stored); + + service.getNCDScreeningDetails(1L, 2L); + + assertTrue(stored.getNcdScreeningConditionList().isEmpty()); + } + + @Test + void updateNCDScreeningDetails_flattensTheConditionsBeforeUpdating() { + NCDScreening screening = screeningWithConditions(); + when(ncdScreeningRepo.updateNCDScreeningDetails(anyString(), anyString(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateNCDScreeningDetails(screening)); + assertEquals("1,2", screening.getNcdScreeningConditionID()); + } + } + + @Nested + @DisplayName("NCDSCreeningDoctorServiceImpl") + class DoctorService { + + @Mock + private PrescriptionDetailRepo prescriptionDetailRepo; + @Mock + private CommonDoctorServiceImpl commonDoctorServiceImpl; + @Mock + private TeleConsultationServiceImpl teleConsultationServiceImpl; + @Mock + private CommonNurseServiceImpl commonNurseServiceImpl; + + @InjectMocks + private NCDSCreeningDoctorServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private String doctorRequest() { + return "{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"providerServiceMapID\":4," + + "\"createdBy\":\"doctor\",\"findings\":{},\"investigation\":{\"laboratoryList\":[{}]," + + "\"externalInvestigations\":\"MRI\"},\"diagnosis\":{\"prescriptionID\":7}," + + "\"prescription\":[{\"drugID\":1}],\"refer\":{}}"; + } + + private Map drugResult() { + Map result = new HashMap<>(); + result.put("count", 1); + result.put("prescribedDrugIDs", Collections.singletonList(9L)); + return result; + } + + private void stubSuccessfulUpdate() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + } + + @Test + void updateDoctorData_updatesEverySectionAndAdvancesTheFlow() throws Exception { + stubSuccessfulUpdate(); + + assertEquals(1, service.updateDoctorData(json(doctorRequest()))); + } + + @Test + void updateDoctorData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1, service.updateDoctorData(json("{\"investigation\":{}}"))); + } + + @Test + void updateDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + stubSuccessfulUpdate(); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, () -> service.updateDoctorData(json(doctorRequest()))); + } + + @Test + void updateDoctorData_failsWhenASectionCouldNotBeUpdated() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(0); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + + assertThrows(RuntimeException.class, () -> service.updateDoctorData(json(doctorRequest()))); + } + + @Test + void getNCDDiagnosisData_splitsTheStoredDiagnosisBackIntoAList() { + PrescriptionDetail stored = new PrescriptionDetail(); + stored.setDiagnosisProvided("Diabetes || Hypertension"); + stored.setDiagnosisProvided_SCTCode("111 || 222"); + when(prescriptionDetailRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)) + .thenReturn(new ArrayList<>(Arrays.asList(stored))); + + String result = service.getNCDDiagnosisData(1L, 2L); + + assertEquals(2, stored.getProvisionalDiagnosisList().size()); + assertTrue(result.contains("Diabetes"), result); + } + + @Test + void getNCDDiagnosisData_returnsAnEmptyDiagnosisWhenNoneWasRecorded() { + when(prescriptionDetailRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(new ArrayList<>()); + + assertEquals("{}", service.getNCDDiagnosisData(1L, 2L)); + } + + @Test + void getNCDDiagnosisData_leavesTheDiagnosisListUnsetWhenNoTermWasCoded() { + PrescriptionDetail stored = new PrescriptionDetail(); + when(prescriptionDetailRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)) + .thenReturn(new ArrayList<>(Arrays.asList(stored))); + + service.getNCDDiagnosisData(1L, 2L); + + assertNull(stored.getProvisionalDiagnosisList()); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/ncdscreening/NCDScreeningServiceImplTest.java b/src/test/java/com/iemr/mmu/service/ncdscreening/NCDScreeningServiceImplTest.java new file mode 100644 index 00000000..0128801f --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/ncdscreening/NCDScreeningServiceImplTest.java @@ -0,0 +1,639 @@ +/* +* 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.mmu.service.ncdscreening; + +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.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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +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.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus; +import com.iemr.mmu.data.ncdScreening.IDRSData; +import com.iemr.mmu.data.nurse.CommonUtilityClass; +import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo; +import com.iemr.mmu.repo.nurse.BenVisitDetailRepo; +import com.iemr.mmu.repo.nurse.ncdscreening.IDRSDataRepo; +import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo; +import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonServiceImpl; +import com.iemr.mmu.service.labtechnician.LabTechnicianServiceImpl; +import com.iemr.mmu.service.tele_consultation.TeleConsultationServiceImpl; + +class NCDScreeningServiceImplTest { + + @Mock + private NCDScreeningNurseServiceImpl ncdScreeningNurseServiceImpl; + @Mock + private CommonNurseServiceImpl commonNurseServiceImpl; + @Mock + private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl; + @Mock + private CommonDoctorServiceImpl commonDoctorServiceImpl; + @Mock + private LabTechnicianServiceImpl labTechnicianServiceImpl; + @Mock + private CommonServiceImpl commonServiceImpl; + @Mock + private TeleConsultationServiceImpl teleConsultationServiceImpl; + @Mock + private NCDSCreeningDoctorServiceImpl ncdSCreeningDoctorServiceImpl; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + @Mock + private BenVisitDetailRepo benVisitDetailRepo; + @Mock + private IDRSDataRepo iDrsDataRepo; + @Mock + private PrescriptionDetailRepo prescriptionDetailRepo; + + @InjectMocks + private NCDScreeningServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + private static String visitDetailsBlock() { + return "\"visitDetails\":{\"visitDetails\":{\"beneficiaryRegID\":1,\"visitReason\":\"Screening\"," + + "\"visitCategory\":\"NCD screening\"},\"chiefComplaints\":[{\"chiefComplaintID\":1}]}"; + } + + @Nested + @DisplayName("saving nurse data") + class NurseSave { + + @Test + void saveNCDScreeningNurseData_rejectsARequestWithoutVisitDetails() { + assertThrows(Exception.class, () -> service.saveNCDScreeningNurseData(json("{}"), "auth")); + assertThrows(Exception.class, () -> service.saveNCDScreeningNurseData(null, "auth")); + } + + @Test + void saveNCDScreeningNurseData_skipsTheVisitWhenTheNurseAlreadySavedThisFlow() throws Exception { + when(beneficiaryFlowStatusRepo.checkExistData(any(), any())).thenReturn(new BeneficiaryFlowStatus()); + + assertEquals(0L, service.saveNCDScreeningNurseData(json("{" + visitDetailsBlock() + "}"), "auth")); + verify(commonNurseServiceImpl, never()).saveBeneficiaryVisitDetails(any()); + } + + @Test + void saveNCDScreeningNurseData_returnsZeroWhenTheVisitWasNotCreated() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1); + + assertEquals(0L, service.saveNCDScreeningNurseData(json("{" + visitDetailsBlock() + "}"), "auth")); + } + + @Test + void saveNCDScreeningNurseData_savesEverySectionAndAdvancesTheBeneficiaryFlow() throws Exception { + stubVisitCreation(); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveIDRS(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePhysicalActivity(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1); + + String request = "{" + visitDetailsBlock() + + ",\"historyDetails\":{\"physicalActivityHistory\":{\"activityType\":\"Walking\"}}," + + "\"vitalDetails\":{},\"idrsDetails\":{}}"; + + assertEquals(1L, service.saveNCDScreeningNurseData(json(request), "auth")); + } + + @Test + void saveNCDScreeningNurseData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + stubVisitCreation(); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(0); + + String request = "{" + visitDetailsBlock() + ",\"historyDetails\":{},\"vitalDetails\":{}," + + "\"idrsDetails\":{}}"; + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.saveNCDScreeningNurseData(json(request), "auth")); + assertTrue(thrown.getMessage().contains("Beneficiary status update failed")); + } + + @Test + void saveNCDScreeningNurseData_failsWhenASectionCouldNotBeSaved() throws Exception { + stubVisitCreation(); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(null); + + String request = "{" + visitDetailsBlock() + ",\"historyDetails\":{},\"vitalDetails\":{}," + + "\"idrsDetails\":{}}"; + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.saveNCDScreeningNurseData(json(request), "auth")); + assertEquals("Error occurred while saving data", thrown.getMessage()); + } + + private void stubVisitCreation() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + } + + @Test + void saveBenVisitDetails_stampsTheVisitOntoEveryChiefComplaint() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + + Map result = service.saveBenVisitDetails( + json("{" + visitDetailsBlock() + "}").getAsJsonObject("visitDetails"), new CommonUtilityClass()); + + assertEquals(5L, result.get("visitID")); + verify(commonNurseServiceImpl).saveBenChiefComplaints(any()); + } + + @Test + void saveBenVisitDetails_returnsNothingWhenTheVisitBlockIsMissing() throws Exception { + assertTrue(service.saveBenVisitDetails(json("{}"), new CommonUtilityClass()).isEmpty()); + assertTrue(service.saveBenVisitDetails(null, new CommonUtilityClass()).isEmpty()); + } + } + + @Nested + @DisplayName("saving a teleconsultation referral") + class TeleconsultationReferral { + + private String tmReferredRequest(String prescription) { + return "{\"isTMCDone\":true,\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3," + + "\"providerServiceMapID\":4,\"prescription\":" + prescription + ",\"refer\":{}}"; + } + + @Test + void saveNCDScreeningNurseData_storesTheReferralAndAdvancesTheFlow() throws Exception { + when(commonDoctorServiceImpl.saveBenReferDetailsTMreferred(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowtableAfterNurseSaveForTMReferred(any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.saveNCDScreeningNurseData(json(tmReferredRequest("[]")), "auth")); + } + + @Test + void saveNCDScreeningNurseData_storesEveryPrescribedDrugAgainstTheReferralPrescription() throws Exception { + when(prescriptionDetailRepo.getPrescriptionID(3L)).thenReturn(7L); + Map drugResult = new HashMap<>(); + drugResult.put("count", 1); + drugResult.put("prescribedDrugIDs", Collections.singletonList(9L)); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult); + when(commonDoctorServiceImpl.saveBenReferDetailsTMreferred(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowtableAfterNurseSaveForTMReferred(any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, + service.saveNCDScreeningNurseData(json(tmReferredRequest("[{\"drugID\":1}]")), "auth")); + } + + @Test + void saveNCDScreeningNurseData_failsWhenTheReferralFlowCouldNotBeAdvanced() throws Exception { + when(commonDoctorServiceImpl.saveBenReferDetailsTMreferred(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowtableAfterNurseSaveForTMReferred(any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, + () -> service.saveNCDScreeningNurseData(json(tmReferredRequest("[]")), "auth")); + } + } + + @Nested + @DisplayName("saving the individual nurse sections") + class NurseSections { + + @Test + void saveBenNCDCareHistoryDetails_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenNCDCareHistoryDetails(json("{}"), 1L, 2L)); + assertEquals(1L, service.saveBenNCDCareHistoryDetails(null, 1L, 2L)); + } + + @Test + void saveBenNCDCareHistoryDetails_savesEverySectionThatWasSent() throws Exception { + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenComorbidConditions(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenMedicationHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveFemaleObstetricHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenMenstrualHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenFamilyHistoryNCDScreening(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePersonalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.saveAllergyHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildOptionalVaccineDetail(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveImmunizationHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildDevelopmentHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildFeedingHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePerinatalHistory(any())).thenReturn(1L); + + String history = "{\"pastHistory\":{},\"comorbidConditions\":{}," + + "\"medicationHistory\":{\"medicationHistoryList\":[{\"currentMedication\":\"x\"}]}," + + "\"femaleObstetricHistory\":{},\"menstrualHistory\":{},\"familyHistory\":{}," + + "\"personalHistory\":{},\"childVaccineDetails\":{},\"immunizationHistory\":{}," + + "\"developmentHistory\":{},\"feedingHistory\":{},\"perinatalHistroy\":{}}"; + + assertEquals(1L, service.saveBenNCDCareHistoryDetails(json(history), 1L, 2L)); + } + + @Test + void saveBenNCDCareHistoryDetails_reportsFailureWhenASectionCouldNotBeSaved() throws Exception { + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(0L); + assertNull(service.saveBenNCDCareHistoryDetails(json("{\"pastHistory\":{}}"), 1L, 2L)); + } + + @Test + void saveidrsDetails_joinsEveryAnsweredQuestionOntoOneRow() throws Exception { + when(commonNurseServiceImpl.saveIDRS(any())).thenReturn(1L); + + String idrs = "{\"questionArray\":[{\"idrsQuestionID\":1,\"question\":\"Q1\",\"answer\":\"Yes\"," + + "\"diseaseQuestionType\":\"Diabetes\"},{\"idrsQuestionID\":2,\"question\":\"Q2\"," + + "\"answer\":\"No\",\"diseaseQuestionType\":\"Hypertension\"}]," + + "\"suspectArray\":[\"Diabetes\",\"Hypertension\"],\"confirmArray\":[\"Diabetes\"]}"; + + assertEquals(1L, service.saveidrsDetails(json(idrs), 1L, 2L)); + + ArgumentCaptor saved = ArgumentCaptor.forClass(IDRSData.class); + verify(commonNurseServiceImpl).saveIDRS(saved.capture()); + assertEquals("1||2", saved.getValue().getQuestionIds()); + assertEquals("Q1||Q2", saved.getValue().getQuestion()); + assertEquals("Yes||No", saved.getValue().getAnswer()); + assertEquals("Diabetes||Hypertension", saved.getValue().getDiseaseQuestionType()); + assertEquals("Diabetes,Hypertension", saved.getValue().getSuspectedDisease()); + assertEquals("Diabetes", saved.getValue().getConfirmedDisease()); + } + + @Test + void saveidrsDetails_storesTheScreeningEvenWhenNoQuestionWasAnswered() throws Exception { + when(commonNurseServiceImpl.saveIDRS(any())).thenReturn(1L); + + String idrs = "{\"suspectArray\":[\"Diabetes\"],\"confirmArray\":[\"Diabetes\"]}"; + + assertEquals(1L, service.saveidrsDetails(json(idrs), 1L, 2L)); + assertNull(service.saveidrsDetails(null, 1L, 2L)); + } + + @Test + void savePhysicalActivityDetails_storesTheActivityWhenOneWasReported() throws Exception { + when(commonNurseServiceImpl.savePhysicalActivity(any())).thenReturn(1L); + + assertEquals(1L, service.savePhysicalActivityDetails(json("{\"activityType\":\"Walking\"}"), 1L, 2L)); + assertNull(service.savePhysicalActivityDetails(json("{}"), 1L, 2L)); + assertNull(service.savePhysicalActivityDetails(null, 1L, 2L)); + } + + @Test + void saveBenNCDCareVitalDetails_savesAnthropometryAndVitalsTogether() throws Exception { + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(5L); + + assertEquals(4L, service.saveBenNCDCareVitalDetails(json("{}"), 1L, 2L)); + + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(null); + assertNull(service.saveBenNCDCareVitalDetails(json("{}"), 1L, 2L)); + assertNull(service.saveBenNCDCareVitalDetails(null, 1L, 2L)); + } + + @Test + void saveNCDScreeningVitalDetails_readsTheVitalsOutOfTheScreeningBlock() throws Exception { + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(5L); + + assertEquals(4L, service.saveNCDScreeningVitalDetails(json("{\"ncdScreeningDetails\":{}}"), 1L, 2L)); + + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(null); + assertNull(service.saveNCDScreeningVitalDetails(json("{\"ncdScreeningDetails\":{}}"), 1L, 2L)); + } + } + + @Nested + @DisplayName("updating nurse data") + class NurseUpdates { + + @Test + void updateNurseNCDScreeningDetails_storesTheAttachedFilesAndAdvancesTheFlow() throws Exception { + when(ncdScreeningNurseServiceImpl.updateNCDScreeningDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(1); + + String request = "{\"beneficiaryRegID\":1,\"visitCode\":2,\"benFlowID\":3," + + "\"nextScreeningDate\":\"2024-01-01T10:00:00Z\",\"isScreeningComplete\":true," + + "\"fileIDs\":[\"a\",\"b\"]}"; + + assertEquals(1, service.updateNurseNCDScreeningDetails(json(request))); + verify(benVisitDetailRepo).updateFileID("a,b,", 1L, 2L); + verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseUpdateNCD_Screening(3L, 1L, (short) 9); + } + + @Test + void updateNurseNCDScreeningDetails_marksAnIncompleteScreeningAsStillWithTheNurse() throws Exception { + when(ncdScreeningNurseServiceImpl.updateNCDScreeningDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(1); + + assertEquals(1, service.updateNurseNCDScreeningDetails( + json("{\"beneficiaryRegID\":1,\"benFlowID\":3,\"isScreeningComplete\":false}"))); + verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseUpdateNCD_Screening(3L, 1L, + (short) 100); + } + + @Test + void updateNurseNCDScreeningDetails_reportsNoResultWhenAnUpdateCouldNotBeApplied() throws Exception { + when(ncdScreeningNurseServiceImpl.updateNCDScreeningDetails(any())).thenReturn(null); + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(1); + + assertNull(service.updateNurseNCDScreeningDetails(json("{\"beneficiaryRegID\":1}"))); + } + + @Test + void updateBenVitalDetails_updatesAnthropometryAndVitalsTogether() throws Exception { + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(1); + + assertEquals(1, service.updateBenVitalDetails(json("{}"))); + + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(0); + assertEquals(0, service.updateBenVitalDetails(json("{}"))); + assertEquals(1, service.updateBenVitalDetails(null)); + } + + @Test + void UpdateNCDScreeningHistory_updatesTheFamilyAndActivityHistoryTogether() throws Exception { + when(commonNurseServiceImpl.updateBenFamilyHistoryNCDScreening(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenPhysicalActivityHistoryNCDScreening(any())).thenReturn(1); + + String history = "{\"familyHistory\":{},\"physicalActivityHistory\":{},\"personalHistory\":{}}"; + + assertEquals(1, service.UpdateNCDScreeningHistory(json(history))); + verify(commonNurseServiceImpl).updateBenPersonalHistory(any()); + verify(commonNurseServiceImpl).updateBenAllergicHistory(any()); + } + + @Test + void UpdateNCDScreeningHistory_reportsNoUpdateWhenNoHistoryWasSent() throws Exception { + assertEquals(0, service.UpdateNCDScreeningHistory(json("{}"))); + assertEquals(0, service.UpdateNCDScreeningHistory(null)); + } + + @Test + void UpdateIDRSScreen_joinsEveryAnsweredQuestionOntoOneRow() throws Exception { + when(commonNurseServiceImpl.saveIDRS(any())).thenReturn(1L); + + String idrs = "{\"idrsDetails\":{\"questionArray\":[{\"id\":5,\"idrsQuestionID\":1,\"question\":\"Q1\"," + + "\"answer\":\"Yes\",\"diseaseQuestionType\":\"Diabetes\"}]," + + "\"suspectArray\":[\"Diabetes\"],\"confirmArray\":[\"Diabetes\"]}}"; + + assertEquals(1L, service.UpdateIDRSScreen(json(idrs))); + + ArgumentCaptor saved = ArgumentCaptor.forClass(IDRSData.class); + verify(commonNurseServiceImpl).saveIDRS(saved.capture()); + assertEquals(5L, saved.getValue().getId()); + assertEquals("1", saved.getValue().getQuestionIds()); + } + + @Test + void UpdateIDRSScreen_updatesTheSuspectedDiseasesAndScoreWhenNoQuestionWasReanswered() throws Exception { + when(iDrsDataRepo.updateSuspectedDiseases(1L, 2L, "Diabetes")).thenReturn(1); + when(iDrsDataRepo.updateIdrsScore(1L, 2L, 30)).thenReturn(1); + + String idrs = "{\"idrsDetails\":{\"beneficiaryRegID\":1,\"visitCode\":2," + + "\"suspectArray\":[\"Diabetes\"],\"idrsScore\":30}}"; + + assertEquals(1L, service.UpdateIDRSScreen(json(idrs))); + } + + @Test + void UpdateIDRSScreen_doesNothingWithoutAScreeningBlock() throws Exception { + assertNull(service.UpdateIDRSScreen(json("{}"))); + assertNull(service.UpdateIDRSScreen(null)); + } + } + + @Nested + @DisplayName("reading the nurse and doctor case sheets") + class Reads { + + @Test + void getNCDScreeningDetails_gathersTheScreeningAnthropometryAndVitals() { + when(ncdScreeningNurseServiceImpl.getNCDScreeningDetails(1L, 2L)).thenReturn("screening"); + when(commonNurseServiceImpl.getBeneficiaryPhysicalAnthropometryDetails(1L, 2L)).thenReturn("anthro"); + when(commonNurseServiceImpl.getBeneficiaryPhysicalVitalDetails(1L, 2L)).thenReturn("vitals"); + + assertTrue(service.getNCDScreeningDetails(1L, 2L).contains("ncdScreeningDetails")); + } + + @Test + void getNCDScreeningDetails_returnsNothingWhenASectionIsMissing() { + when(ncdScreeningNurseServiceImpl.getNCDScreeningDetails(1L, 2L)).thenReturn(null); + assertEquals("{}", service.getNCDScreeningDetails(1L, 2L)); + } + + @Test + void getNcdScreeningVisitCnt_reportsTheNextVisitNumber() { + when(beneficiaryFlowStatusRepo.getNcdScreeningVisitCount(1L)).thenReturn(3L); + assertTrue(service.getNcdScreeningVisitCnt(1L).contains("4")); + } + + @Test + void getBenVisitDetailsFrmNurseNCDScreening_gathersTheVisitAndItsComplaints() throws Exception { + when(commonNurseServiceImpl.getCSVisitDetails(1L, 2L)).thenReturn(null); + when(commonNurseServiceImpl.getBenChiefComplaints(1L, 2L)).thenReturn("[]"); + + assertTrue(service.getBenVisitDetailsFrmNurseNCDScreening(1L, 2L) + .contains("NCDScreeningNurseVisitDetail")); + } + + @Test + void getBenHistoryDetails_gathersTheFamilyActivityAndPersonalHistory() { + when(commonNurseServiceImpl.getFamilyHistoryDetail(1L, 2L)) + .thenReturn(new com.iemr.mmu.data.anc.BenFamilyHistory()); + + assertTrue(service.getBenHistoryDetails(1L, 2L).contains("FamilyHistory")); + verify(commonNurseServiceImpl).getPhysicalActivityType(1L, 2L); + verify(commonNurseServiceImpl).getPersonalHistory(1L, 2L); + } + + @Test + void getBenIdrsDetailsFrmNurse_readsTheStoredScreening() { + when(commonNurseServiceImpl.getBeneficiaryIdrsDetails(1L, 2L)).thenReturn(new IDRSData()); + assertTrue(service.getBenIdrsDetailsFrmNurse(1L, 2L).contains("IDRSDetail")); + } + + @Test + void getBeneficiaryVitalDetails_gathersAnthropometryAndVitals() { + when(commonNurseServiceImpl.getBeneficiaryPhysicalAnthropometryDetails(1L, 2L)).thenReturn("a"); + when(commonNurseServiceImpl.getBeneficiaryPhysicalVitalDetails(1L, 2L)).thenReturn("v"); + + assertTrue(service.getBeneficiaryVitalDetails(1L, 2L).contains("benAnthropometryDetail")); + } + + @Test + void getBenNCDScreeningNurseData_combinesTheVitalsHistoryAndScreening() { + String result = service.getBenNCDScreeningNurseData(1L, 2L); + + assertTrue(result.contains("vitals")); + assertTrue(result.contains("history")); + assertTrue(result.contains("idrs")); + } + + @Test + void getBenCaseRecordFromDoctorNCDScreening_gathersEveryDoctorSection() throws Exception { + when(commonDoctorServiceImpl.getFindingsDetails(1L, 2L)).thenReturn("findings"); + when(ncdSCreeningDoctorServiceImpl.getNCDDiagnosisData(1L, 2L)).thenReturn("diagnosis"); + when(commonDoctorServiceImpl.getInvestigationDetails(1L, 2L)).thenReturn("investigation"); + when(commonDoctorServiceImpl.getPrescribedDrugs(1L, 2L)).thenReturn("prescription"); + when(commonDoctorServiceImpl.getReferralDetails(1L, 2L)).thenReturn("refer"); + when(labTechnicianServiceImpl.getLabResultDataForBen(1L, 2L)).thenReturn(new ArrayList<>()); + when(commonNurseServiceImpl.getGraphicalTrendData(1L, "ncdCare")).thenReturn(new HashMap<>()); + when(labTechnicianServiceImpl.getLast_3_ArchivedTestVisitList(1L, 2L)).thenReturn("[]"); + + assertTrue(service.getBenCaseRecordFromDoctorNCDScreening(1L, 2L).contains("findings")); + } + } + + @Nested + @DisplayName("saving doctor data") + class DoctorData { + + private String doctorRequest(String extra) { + return "{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"providerServiceMapID\":4," + + "\"createdBy\":\"doctor\",\"findings\":{},\"investigation\":{\"laboratoryList\":[{}]}," + + "\"diagnosis\":{},\"prescription\":[{\"drugID\":1}],\"refer\":{}" + extra + "}"; + } + + private Map drugResult() { + Map result = new HashMap<>(); + result.put("count", 1); + result.put("prescribedDrugIDs", Collections.singletonList(9L)); + return result; + } + + private void stubSuccessfulDoctorSave() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenPrescription(any())).thenReturn(7L); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + } + + @Test + void saveDoctorData_savesEverySectionAndAdvancesTheFlow() throws Exception { + stubSuccessfulDoctorSave(); + + assertEquals(1L, service.saveDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void saveDoctorData_storesTheFilesAttachedToTheVisit() throws Exception { + stubSuccessfulDoctorSave(); + + String request = "{\"beneficiaryRegID\":1,\"visitCode\":3,\"findings\":{}," + + "\"investigation\":{\"laboratoryList\":[{}]},\"diagnosis\":{}," + + "\"prescription\":[{\"drugID\":1}],\"refer\":{}," + + "\"visitDetails\":{\"visitDetails\":{\"fileIDs\":[\"a\",\"b\"]}}}"; + + assertEquals(1L, service.saveDoctorData(json(request), "auth")); + verify(benVisitDetailRepo).updateFileID(anyString(), any(), any()); + } + + @Test + void saveDoctorData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + when(commonNurseServiceImpl.saveBenPrescription(any())).thenReturn(7L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.saveDoctorData(json("{\"investigation\":{}}"), "auth")); + } + + @Test + void saveDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + stubSuccessfulDoctorSave(); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, () -> service.saveDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void saveDoctorData_failsWhenASectionCouldNotBeSaved() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(0); + when(commonNurseServiceImpl.saveBenPrescription(any())).thenReturn(7L); + + assertThrows(RuntimeException.class, () -> service.saveDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void saveDoctorData_booksTheSpecialistSlotBeforeRaisingATeleconsultationRequest() throws Exception { + stubSuccessfulDoctorSave(); + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(1); + when(teleConsultationServiceImpl.createTCRequest(any())).thenReturn(1); + + String extra = ",\"serviceID\":4,\"tcRequest\":{\"userID\":5,\"allocationDate\":\"2024-01-01\"," + + "\"fromTime\":\"10:00:00\",\"toTime\":\"10:30:00\"}"; + + assertEquals(1L, service.saveDoctorData(json(doctorRequest(extra)), "auth")); + verify(teleConsultationServiceImpl).createTCRequest(any()); + } + + @Test + void saveDoctorData_failsWhenTheSpecialistSlotCouldNotBeBooked() throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(0); + + String extra = ",\"serviceID\":4,\"tcRequest\":{\"userID\":5,\"allocationDate\":\"2024-01-01\"," + + "\"fromTime\":\"10:00:00\",\"toTime\":\"10:30:00\"}"; + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.saveDoctorData(json(doctorRequest(extra)), "auth")); + assertEquals("Error while booking slot.", thrown.getMessage()); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/pnc/PNCDoctorServiceImplTest.java b/src/test/java/com/iemr/mmu/service/pnc/PNCDoctorServiceImplTest.java index 1af79328..1820caa8 100644 --- a/src/test/java/com/iemr/mmu/service/pnc/PNCDoctorServiceImplTest.java +++ b/src/test/java/com/iemr/mmu/service/pnc/PNCDoctorServiceImplTest.java @@ -21,6 +21,174 @@ */ package com.iemr.mmu.service.pnc; -public class PNCDoctorServiceImplTest { - +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.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.pnc.PNCDiagnosis; +import com.iemr.mmu.repo.nurse.pnc.PNCDiagnosisRepo; +import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo; +import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl; + +class PNCDoctorServiceImplTest { + + @Mock + private PNCDiagnosisRepo pncDiagnosisRepo; + @Mock + private PrescriptionDetailRepo prescriptionDetailRepo; + @Mock + private CommonDoctorServiceImpl commonDoctorServiceImpl; + + @InjectMocks + private PNCDoctorServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + /** A diagnosis with two provisional and two confirmatory terms, one of them un-coded. */ + private static String diagnosisRequest() { + return "{\"beneficiaryRegID\":1,\"visitCode\":2,\"createdBy\":\"doctor\"," + + "\"provisionalDiagnosisList\":[{\"term\":\"Anaemia\",\"conceptID\":\"111\"}," + + "{\"term\":\"Fever\"},{\"conceptID\":\"333\"}]," + + "\"confirmatoryDiagnosisList\":[{\"term\":\"Anaemia\"}," + + "{\"term\":\"Fever\",\"conceptID\":\"222\"}]}"; + } + + @Test + @DisplayName("a saved diagnosis joins every named term and its concept id") + void saveBenPNCDiagnosis_joinsEveryNamedTerm() throws Exception { + PNCDiagnosis stored = new PNCDiagnosis(); + stored.setID(5L); + when(pncDiagnosisRepo.save(any())).thenReturn(stored); + + assertEquals(5L, service.saveBenPNCDiagnosis(json(diagnosisRequest()), 7L)); + + ArgumentCaptor saved = ArgumentCaptor.forClass(PNCDiagnosis.class); + org.mockito.Mockito.verify(pncDiagnosisRepo).save(saved.capture()); + assertEquals(7L, saved.getValue().getPrescriptionID()); + // The third entry carries no term, so the separator the second one appended is + // left dangling. + assertEquals("Anaemia || Fever || ", saved.getValue().getProvisionalDiagnosis()); + assertEquals("111 || N/A || ", saved.getValue().getProvisionalDiagnosisSCTCode()); + assertEquals("Anaemia || Fever", saved.getValue().getConfirmatoryDiagnosis()); + assertEquals("N/A || 222", saved.getValue().getConfirmatoryDiagnosisSCTCode()); + } + + @Test + @DisplayName("a diagnosis with no terms is stored with empty diagnosis fields") + void saveBenPNCDiagnosis_storesEmptyDiagnosisFieldsWhenNoTermWasGiven() throws Exception { + PNCDiagnosis stored = new PNCDiagnosis(); + stored.setID(5L); + when(pncDiagnosisRepo.save(any())).thenReturn(stored); + + assertEquals(5L, service.saveBenPNCDiagnosis(json("{\"beneficiaryRegID\":1}"), 7L)); + + ArgumentCaptor saved = ArgumentCaptor.forClass(PNCDiagnosis.class); + org.mockito.Mockito.verify(pncDiagnosisRepo).save(saved.capture()); + assertEquals("", saved.getValue().getProvisionalDiagnosis()); + } + + @Test + @DisplayName("a diagnosis that was not persisted reports no id") + void saveBenPNCDiagnosis_reportsNoIdWhenNothingWasStored() throws Exception { + PNCDiagnosis stored = new PNCDiagnosis(); + stored.setID(0L); + when(pncDiagnosisRepo.save(any())).thenReturn(stored); + + assertNull(service.saveBenPNCDiagnosis(json(diagnosisRequest()), 7L)); + } + + @Test + @DisplayName("a stored diagnosis is read back with its terms split into lists") + void getPNCDiagnosisDetails_splitsTheStoredTermsBackIntoLists() { + PNCDiagnosis stored = new PNCDiagnosis(); + stored.setProvisionalDiagnosis("Anaemia || Fever"); + stored.setProvisionalDiagnosisSCTCode("111 || N/A"); + stored.setConfirmatoryDiagnosis("Anaemia"); + stored.setConfirmatoryDiagnosisSCTCode("N/A"); + when(pncDiagnosisRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)) + .thenReturn(new ArrayList<>(Arrays.asList(stored))); + when(prescriptionDetailRepo.getExternalinvestigationForVisitCode(1L, 2L)).thenReturn("X-ray"); + + String result = service.getPNCDiagnosisDetails(1L, 2L); + + assertTrue(result.contains("X-ray"), result); + assertEquals(2, stored.getProvisionalDiagnosisList().size()); + assertEquals(1, stored.getConfirmatoryDiagnosisList().size()); + } + + @Test + @DisplayName("a beneficiary with no diagnosis reads back as an empty diagnosis") + void getPNCDiagnosisDetails_returnsAnEmptyDiagnosisWhenNoneWasRecorded() { + when(pncDiagnosisRepo.findByBeneficiaryRegIDAndVisitCode(1L, 2L)).thenReturn(new ArrayList<>()); + + assertEquals("{}", service.getPNCDiagnosisDetails(1L, 2L)); + } + + @Test + @DisplayName("an already-stored diagnosis is updated in place") + void updateBenPNCDiagnosis_updatesTheStoredRow() throws Exception { + PNCDiagnosis diagnosis = new PNCDiagnosis(); + diagnosis.setBeneficiaryRegID(1L); + diagnosis.setVisitCode(2L); + diagnosis.setPrescriptionID(7L); + when(pncDiagnosisRepo.getPNCDiagnosisStatus(1L, 2L, 7L)).thenReturn("P"); + when(pncDiagnosisRepo.updatePNCDiagnosis(anyString(), anyString(), any(), any(), any(), any(), any(), + eq("U"), anyLong(), anyLong(), anyString(), any(), anyString(), any(), anyLong())).thenReturn(1); + + assertEquals(1, service.updateBenPNCDiagnosis(diagnosis)); + } + + @Test + @DisplayName("a diagnosis that was never stored is inserted instead") + void updateBenPNCDiagnosis_insertsAFreshRowWhenNoneIsStoredYet() throws Exception { + PNCDiagnosis diagnosis = new PNCDiagnosis(); + PNCDiagnosis stored = new PNCDiagnosis(); + stored.setID(5L); + when(pncDiagnosisRepo.getPNCDiagnosisStatus(any(), any(), any())).thenReturn(null); + when(pncDiagnosisRepo.save(diagnosis)).thenReturn(stored); + + assertEquals(1, service.updateBenPNCDiagnosis(diagnosis)); + + stored.setID(0L); + assertEquals(0, service.updateBenPNCDiagnosis(diagnosis)); + } + + @Test + @DisplayName("an update joins every named term and its concept id") + void updateBenPNCDiagnosis_joinsEveryNamedTerm() throws Exception { + PNCDiagnosis diagnosis = com.iemr.mmu.utils.mapper.InputMapper.gson().fromJson(diagnosisRequest(), + PNCDiagnosis.class); + when(pncDiagnosisRepo.getPNCDiagnosisStatus(any(), any(), any())).thenReturn("N"); + when(pncDiagnosisRepo.updatePNCDiagnosis(anyString(), anyString(), any(), any(), any(), any(), any(), + eq("N"), anyLong(), anyLong(), anyString(), any(), anyString(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBenPNCDiagnosis(diagnosis)); + assertEquals("Anaemia || Fever || ", diagnosis.getProvisionalDiagnosis()); + assertEquals("111 || N/A || ", diagnosis.getProvisionalDiagnosisSCTCode()); + } } diff --git a/src/test/java/com/iemr/mmu/service/pnc/PNCServiceImplTest.java b/src/test/java/com/iemr/mmu/service/pnc/PNCServiceImplTest.java index fc109a3f..474af902 100644 --- a/src/test/java/com/iemr/mmu/service/pnc/PNCServiceImplTest.java +++ b/src/test/java/com/iemr/mmu/service/pnc/PNCServiceImplTest.java @@ -19,53 +19,609 @@ * 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.mmu.service.pnc; -import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl; -import com.google.gson.JsonObject; -import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl; -import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl; -import com.iemr.mmu.service.pnc.PNCDoctorServiceImpl; -import com.iemr.mmu.service.pnc.PNCNurseServiceImpl; -import com.iemr.mmu.service.tele_consultation.TeleConsultationServiceImpl; -import com.iemr.mmu.data.nurse.CommonUtilityClass; -import com.iemr.mmu.utils.mapper.InputMapper; -import com.iemr.mmu.service.anc.Utility; -import com.google.gson.JsonArray; +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.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + 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.MockitoAnnotations; -import org.mockito.junit.jupiter.MockitoExtension; -import java.util.HashMap; -import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -public class PNCServiceImplTest { - @Mock - private CommonNurseServiceImpl commonNurseServiceImpl; - @Mock - private CommonDoctorServiceImpl commonDoctorServiceImpl; - @Mock - private PNCNurseServiceImpl pncNurseServiceImpl; - @Mock - private PNCDoctorServiceImpl pncDoctorServiceImpl; - @Mock - private TeleConsultationServiceImpl teleConsultationServiceImpl; - @Mock - private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl; - @InjectMocks - private PNCServiceImpl pncServiceImpl; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - - // Add test methods for each public method in PNCServiceImpl here + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus; +import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo; +import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl; +import com.iemr.mmu.service.labtechnician.LabTechnicianServiceImpl; +import com.iemr.mmu.service.tele_consultation.TeleConsultationServiceImpl; + +class PNCServiceImplTest { + + @Mock + private CommonNurseServiceImpl commonNurseServiceImpl; + @Mock + private CommonDoctorServiceImpl commonDoctorServiceImpl; + @Mock + private PNCNurseServiceImpl pncNurseServiceImpl; + @Mock + private PNCDoctorServiceImpl pncDoctorServiceImpl; + @Mock + private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl; + @Mock + private LabTechnicianServiceImpl labTechnicianServiceImpl; + @Mock + private TeleConsultationServiceImpl teleConsultationServiceImpl; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + + @InjectMocks + private PNCServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + /** The visit block every nurse save starts from. */ + private static String visitDetailsBlock() { + return "\"visitDetails\":{\"visitDetails\":{\"beneficiaryRegID\":1,\"visitReason\":\"New Chief Complaint\"," + + "\"visitCategory\":\"PNC\"},\"chiefComplaints\":[{\"chiefComplaintID\":1}]}"; + } + + @Nested + @DisplayName("saving nurse data") + class NurseSave { + + @Test + void savePNCNurseData_ignoresARequestWithoutVisitDetails() throws Exception { + assertNull(service.savePNCNurseData(null)); + assertNull(service.savePNCNurseData(json("{}"))); + assertNull(service.savePNCNurseData(json("{\"visitDetails\":null}"))); + } + + @Test + void savePNCNurseData_skipsTheVisitWhenTheNurseAlreadySavedThisFlow() throws Exception { + when(beneficiaryFlowStatusRepo.checkExistData(any(), any())).thenReturn(new BeneficiaryFlowStatus()); + + assertEquals(0L, service.savePNCNurseData(json("{" + visitDetailsBlock() + "}"))); + verify(commonNurseServiceImpl, never()).saveBeneficiaryVisitDetails(any()); + } + + @Test + void savePNCNurseData_returnsZeroWhenTheVisitWasNotCreated() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1); + + assertEquals(0L, service.savePNCNurseData(json("{" + visitDetailsBlock() + "}"))); + } + + @Test + void savePNCNurseData_savesEverySectionAndAdvancesTheBeneficiaryFlow() throws Exception { + stubVisitCreation(); + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(1L); + when(pncNurseServiceImpl.saveBenPncCareDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePhyGeneralExamination(any())).thenReturn(1L); + when(commonBenStatusFlowServiceImpl.updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any())).thenReturn(1); + + String request = "{" + visitDetailsBlock() + ",\"historyDetails\":{\"pastHistory\":{}}," + + "\"pNCDeatils\":{},\"vitalDetails\":{}," + + "\"examinationDetails\":{\"generalExamination\":{}}}"; + + assertEquals(1L, service.savePNCNurseData(json(request))); + verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any()); + } + + @Test + void savePNCNurseData_leavesTheFlowUntouchedWhenASectionFailsToSave() throws Exception { + stubVisitCreation(); + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(0L); + + String request = "{" + visitDetailsBlock() + ",\"historyDetails\":{\"pastHistory\":{}}}"; + + assertNull(service.savePNCNurseData(json(request))); + verify(commonBenStatusFlowServiceImpl, never()).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), + anyLong(), anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any()); + } + + private void stubVisitCreation() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + } + + @Test + void saveBenVisitDetails_stampsTheVisitOntoEveryChiefComplaint() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + + Map result = service.saveBenVisitDetails( + json("{" + visitDetailsBlock() + "}").getAsJsonObject("visitDetails"), + new com.iemr.mmu.data.nurse.CommonUtilityClass()); + + assertEquals(5L, result.get("visitID")); + assertEquals(6L, result.get("visitCode")); + verify(commonNurseServiceImpl).saveBenChiefComplaints(any()); + } + + @Test + void saveBenVisitDetails_returnsNothingWhenTheVisitBlockIsMissing() throws Exception { + assertTrue(service.saveBenVisitDetails(json("{}"), new com.iemr.mmu.data.nurse.CommonUtilityClass()) + .isEmpty()); + assertTrue(service.saveBenVisitDetails(null, new com.iemr.mmu.data.nurse.CommonUtilityClass()).isEmpty()); + } + } + + @Nested + @DisplayName("saving the individual nurse sections") + class NurseSections { + + @Test + void saveBenPNCHistoryDetails_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenPNCHistoryDetails(json("{}"), 1L, 2L)); + } + + @Test + void saveBenPNCHistoryDetails_savesEverySectionThatWasSent() throws Exception { + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenComorbidConditions(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenMedicationHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePersonalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.saveAllergyHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenFamilyHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenMenstrualHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.saveFemaleObstetricHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveImmunizationHistory(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveChildOptionalVaccineDetail(any())).thenReturn(1L); + + String history = "{\"pastHistory\":{},\"comorbidConditions\":{}," + + "\"medicationHistory\":{\"medicationHistoryList\":[{\"currentMedication\":\"x\"}]},\"personalHistory\":{}," + + "\"familyHistory\":{},\"menstrualHistory\":{},\"femaleObstetricHistory\":{}," + + "\"immunizationHistory\":{},\"childVaccineDetails\":{}}"; + + assertEquals(1L, service.saveBenPNCHistoryDetails(json(history), 1L, 2L)); + } + + @Test + void saveBenPNCHistoryDetails_treatsAnEmptyMedicationListAsAlreadyDone() throws Exception { + String history = "{\"medicationHistory\":{\"medicationHistoryList\":[]}}"; + assertEquals(1L, service.saveBenPNCHistoryDetails(json(history), 1L, 2L)); + } + + @Test + void saveBenPNCHistoryDetails_reportsFailureWhenASectionCouldNotBeSaved() throws Exception { + when(commonNurseServiceImpl.saveBenPastHistory(any())).thenReturn(0L); + assertNull(service.saveBenPNCHistoryDetails(json("{\"pastHistory\":{}}"), 1L, 2L)); + } + + @Test + void saveBenPNCDetails_savesTheCareBlockWhenOneWasSent() throws Exception { + when(pncNurseServiceImpl.saveBenPncCareDetails(any())).thenReturn(3L); + assertEquals(3L, service.saveBenPNCDetails(json("{\"pNCDeatils\":{}}"), 1L, 2L)); + } + + @Test + void saveBenPNCDetails_treatsAnAbsentCareBlockAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenPNCDetails(json("{}"), 1L, 2L)); + } + + @Test + void saveBenPNCVitalDetails_savesAnthropometryAndVitalsTogether() throws Exception { + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(5L); + + assertEquals(4L, service.saveBenPNCVitalDetails(json("{}"), 1L, 2L)); + } + + @Test + void saveBenPNCVitalDetails_reportsFailureWhenTheVitalsCouldNotBeSaved() throws Exception { + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(4L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(null); + + assertNull(service.saveBenPNCVitalDetails(json("{}"), 1L, 2L)); + assertNull(service.saveBenPNCVitalDetails(null, 1L, 2L)); + } + + @Test + void saveBenExaminationDetails_savesEverySystemThatWasExamined() throws Exception { + when(commonNurseServiceImpl.savePhyGeneralExamination(any())).thenReturn(1L); + when(commonNurseServiceImpl.savePhyHeadToToeExamination(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveSysGastrointestinalExamination(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveSysCardiovascularExamination(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveSysRespiratoryExamination(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveSysCentralNervousExamination(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveSysMusculoskeletalSystemExamination(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveSysGenitourinarySystemExamination(any())).thenReturn(1L); + + String examination = "{\"generalExamination\":{},\"headToToeExamination\":{}," + + "\"gastroIntestinalExamination\":{},\"cardioVascularExamination\":{}," + + "\"respiratorySystemExamination\":{},\"centralNervousSystemExamination\":{}," + + "\"musculoskeletalSystemExamination\":{},\"genitoUrinarySystemExamination\":{}}"; + + assertEquals(1L, service.saveBenExaminationDetails(json(examination), 1L, 2L)); + } + + @Test + void saveBenExaminationDetails_treatsEveryAbsentSystemAsAlreadyDone() throws Exception { + assertEquals(1L, service.saveBenExaminationDetails(json("{}"), 1L, 2L)); + } + + @Test + void saveBenExaminationDetails_reportsFailureWhenASystemCouldNotBeSaved() throws Exception { + when(commonNurseServiceImpl.savePhyGeneralExamination(any())).thenReturn(0L); + assertNull(service.saveBenExaminationDetails(json("{\"generalExamination\":{}}"), 1L, 2L)); + } + } + + @Nested + @DisplayName("reading the nurse and doctor case sheets") + class Reads { + + @Test + void getBenVisitDetailsFrmNursePNC_gathersTheVisitAndItsComplaints() throws Exception { + when(commonNurseServiceImpl.getCSVisitDetails(1L, 2L)).thenReturn(null); + when(commonNurseServiceImpl.getBenChiefComplaints(1L, 2L)).thenReturn("[]"); + + String result = service.getBenVisitDetailsFrmNursePNC(1L, 2L); + + assertTrue(result.contains("PNCNurseVisitDetail")); + assertTrue(result.contains("BenChiefComplaints")); + } + + @Test + void getBenPNCDetailsFrmNursePNC_readsTheCareBlock() { + when(pncNurseServiceImpl.getPNCCareDetails(1L, 2L)).thenReturn("care"); + assertTrue(service.getBenPNCDetailsFrmNursePNC(1L, 2L).contains("care")); + } + + @Test + void getBenHistoryDetails_gathersEveryHistorySection() { + when(commonNurseServiceImpl.getPastHistoryData(1L, 2L)) + .thenReturn(new com.iemr.mmu.data.anc.BenMedHistory()); + + assertTrue(service.getBenHistoryDetails(1L, 2L).contains("PastHistory")); + verify(commonNurseServiceImpl).getFemaleObstetricHistory(1L, 2L); + verify(commonNurseServiceImpl).getFeedingHistory(1L, 2L); + } + + @Test + void getBeneficiaryVitalDetails_gathersAnthropometryAndVitals() { + when(commonNurseServiceImpl.getBeneficiaryPhysicalAnthropometryDetails(1L, 2L)).thenReturn("a"); + when(commonNurseServiceImpl.getBeneficiaryPhysicalVitalDetails(1L, 2L)).thenReturn("v"); + + String result = service.getBeneficiaryVitalDetails(1L, 2L); + assertTrue(result.contains("benAnthropometryDetail")); + assertTrue(result.contains("benPhysicalVitalDetail")); + } + + @Test + void getPNCExaminationDetailsData_gathersEveryExaminedSystem() { + when(commonNurseServiceImpl.getGeneralExaminationData(1L, 2L)) + .thenReturn(new com.iemr.mmu.data.anc.PhyGeneralExamination()); + + assertTrue(service.getPNCExaminationDetailsData(1L, 2L).contains("generalExamination")); + verify(commonNurseServiceImpl).getGenitourinaryExamination(1L, 2L); + } + + @Test + void getBenPNCNurseData_combinesTheFourNurseSections() { + String result = service.getBenPNCNurseData(1L, 2L); + + assertTrue(result.contains("pnc")); + assertTrue(result.contains("history")); + assertTrue(result.contains("vitals")); + assertTrue(result.contains("examination")); + } + + @Test + void getBenCaseRecordFromDoctorPNC_gathersEveryDoctorSection() throws Exception { + when(commonDoctorServiceImpl.getFindingsDetails(1L, 2L)).thenReturn("findings"); + when(pncDoctorServiceImpl.getPNCDiagnosisDetails(1L, 2L)).thenReturn("diagnosis"); + when(commonDoctorServiceImpl.getInvestigationDetails(1L, 2L)).thenReturn("investigation"); + when(commonDoctorServiceImpl.getPrescribedDrugs(1L, 2L)).thenReturn("prescription"); + when(commonDoctorServiceImpl.getReferralDetails(1L, 2L)).thenReturn("refer"); + when(labTechnicianServiceImpl.getLabResultDataForBen(1L, 2L)).thenReturn(new ArrayList<>()); + when(commonNurseServiceImpl.getGraphicalTrendData(1L, "pnc")).thenReturn(new HashMap<>()); + when(labTechnicianServiceImpl.getLast_3_ArchivedTestVisitList(1L, 2L)).thenReturn("[]"); + + String result = service.getBenCaseRecordFromDoctorPNC(1L, 2L); + + assertTrue(result.contains("findings")); + assertTrue(result.contains("LabReport")); + assertTrue(result.contains("GraphData")); + } + } + + @Nested + @DisplayName("updating nurse data") + class NurseUpdates { + + @Test + void updateBenHistoryDetails_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + assertEquals(1, service.updateBenHistoryDetails(json("{}"))); + } + + @Test + void updateBenHistoryDetails_updatesEverySectionThatWasSent() throws Exception { + when(commonNurseServiceImpl.updateBenPastHistoryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenComorbidConditions(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenMedicationHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenPersonalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenAllergicHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateBenFamilyHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateMenstrualHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePastObstetricHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildImmunizationDetail(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildOptionalVaccineDetail(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildFeedingHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePerinatalHistory(any())).thenReturn(1); + when(commonNurseServiceImpl.updateChildDevelopmentHistory(any())).thenReturn(1); + + String history = "{\"pastHistory\":{},\"comorbidConditions\":{},\"medicationHistory\":{}," + + "\"personalHistory\":{},\"familyHistory\":{},\"menstrualHistory\":{}," + + "\"femaleObstetricHistory\":{},\"immunizationHistory\":{\"benChildVaccineDetails\":[{}]}," + + "\"childVaccineDetails\":{},\"feedingHistory\":{},\"perinatalHistroy\":{}," + + "\"developmentHistory\":{}}"; + + assertEquals(1, service.updateBenHistoryDetails(json(history))); + } + + @Test + void updateBenHistoryDetails_reportsFailureWhenASectionCouldNotBeUpdated() throws Exception { + when(commonNurseServiceImpl.updateBenPastHistoryDetails(any())).thenReturn(0); + assertEquals(0, service.updateBenHistoryDetails(json("{\"pastHistory\":{}}"))); + } + + @Test + void updateBenVitalDetails_updatesAnthropometryAndVitalsTogether() throws Exception { + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(1); + + assertEquals(1, service.updateBenVitalDetails(json("{}"))); + } + + @Test + void updateBenVitalDetails_reportsFailureWhenTheVitalsCouldNotBeUpdated() throws Exception { + when(commonNurseServiceImpl.updateANCAnthropometryDetails(any())).thenReturn(1); + when(commonNurseServiceImpl.updateANCPhysicalVitalDetails(any())).thenReturn(0); + + assertEquals(0, service.updateBenVitalDetails(json("{}"))); + assertEquals(1, service.updateBenVitalDetails(null)); + } + + @Test + void updateBenExaminationDetails_updatesEverySystemThatWasSent() throws Exception { + when(commonNurseServiceImpl.updatePhyGeneralExamination(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePhyHeadToToeExamination(any())).thenReturn(1); + when(commonNurseServiceImpl.updateSysGastrointestinalExamination(any())).thenReturn(1); + when(commonNurseServiceImpl.updateSysCardiovascularExamination(any())).thenReturn(1); + when(commonNurseServiceImpl.updateSysRespiratoryExamination(any())).thenReturn(1); + when(commonNurseServiceImpl.updateSysCentralNervousExamination(any())).thenReturn(1); + when(commonNurseServiceImpl.updateSysMusculoskeletalSystemExamination(any())).thenReturn(1); + when(commonNurseServiceImpl.updateSysGenitourinarySystemExamination(any())).thenReturn(1); + + String examination = "{\"generalExamination\":{},\"headToToeExamination\":{}," + + "\"gastroIntestinalExamination\":{},\"cardioVascularExamination\":{}," + + "\"respiratorySystemExamination\":{},\"centralNervousSystemExamination\":{}," + + "\"musculoskeletalSystemExamination\":{},\"genitoUrinarySystemExamination\":{}}"; + + assertEquals(1, service.updateBenExaminationDetails(json(examination))); + } + + @Test + void updateBenExaminationDetails_treatsEveryAbsentSystemAsAlreadyDone() throws Exception { + assertEquals(1, service.updateBenExaminationDetails(json("{}"))); + } + + @Test + void updateBenExaminationDetails_reportsFailureWhenASystemCouldNotBeUpdated() throws Exception { + when(commonNurseServiceImpl.updatePhyGeneralExamination(any())).thenReturn(0); + assertEquals(0, service.updateBenExaminationDetails(json("{\"generalExamination\":{}}"))); + } + + @Test + void updateBenPNCDetails_updatesTheCareBlockWhenOneWasSent() throws Exception { + when(pncNurseServiceImpl.updateBenPNCCareDetails(any())).thenReturn(1); + assertEquals(1, service.updateBenPNCDetails(json("{\"PNCDetails\":{}}"))); + assertEquals(1, service.updateBenPNCDetails(json("{}"))); + } + } + + @Nested + @DisplayName("saving and updating doctor data") + class DoctorData { + + private String doctorRequest(String extra) { + return "{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"providerServiceMapID\":4," + + "\"createdBy\":\"doctor\",\"findings\":{},\"investigation\":{\"laboratoryList\":[{}]}," + + "\"diagnosis\":{},\"prescription\":[{\"drugID\":1}],\"refer\":{}" + extra + "}"; + } + + private void stubSuccessfulDoctorSave() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.savePrescriptionDetailsAndGetPrescriptionID(any(), any(), any(), any(), any(), + any(), any(), any(), any())).thenReturn(7L); + when(pncDoctorServiceImpl.saveBenPNCDiagnosis(any(), any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + Map drugResult = new HashMap<>(); + drugResult.put("count", 1); + drugResult.put("prescribedDrugIDs", Collections.singletonList(9L)); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult); + when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L); + } + + @Test + void savePNCDoctorData_savesEverySectionAndAdvancesTheFlow() throws Exception { + stubSuccessfulDoctorSave(); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.savePNCDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void savePNCDoctorData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.savePrescriptionDetailsAndGetPrescriptionID(any(), any(), any(), any(), any(), + any(), any(), any(), any())).thenReturn(7L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.savePNCDoctorData(json("{\"findings\":{},\"investigation\":{}}"), "auth")); + } + + @Test + void savePNCDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + stubSuccessfulDoctorSave(); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, () -> service.savePNCDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void savePNCDoctorData_failsWhenASectionCouldNotBeSaved() throws Exception { + when(commonDoctorServiceImpl.saveDocFindings(any())).thenReturn(0); + when(commonNurseServiceImpl.savePrescriptionDetailsAndGetPrescriptionID(any(), any(), any(), any(), any(), + any(), any(), any(), any())).thenReturn(7L); + + assertThrows(RuntimeException.class, () -> service.savePNCDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void savePNCDoctorData_booksTheSpecialistSlotBeforeRaisingATeleconsultationRequest() throws Exception { + stubSuccessfulDoctorSave(); + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(1); + when(teleConsultationServiceImpl.createTCRequest(any())).thenReturn(1); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.savePNCDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + verify(teleConsultationServiceImpl).createTCRequest(any()); + } + + @Test + void savePNCDoctorData_failsWhenTheSpecialistSlotCouldNotBeBooked() throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(0); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.savePNCDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + assertEquals("Error while booking slot.", thrown.getMessage()); + } + + private String teleconsultationBlock() { + return ",\"serviceID\":4,\"tcRequest\":{\"userID\":5,\"allocationDate\":\"2024-01-01\"," + + "\"fromTime\":\"10:00:00\",\"toTime\":\"10:30:00\"}"; + } + + @Test + void updatePNCDoctorData_updatesEverySectionAndAdvancesTheFlow() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(pncDoctorServiceImpl.updateBenPNCDiagnosis(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + Map drugResult = new HashMap<>(); + drugResult.put("count", 1); + drugResult.put("prescribedDrugIDs", Collections.singletonList(9L)); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult); + when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.updatePNCDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void updatePNCDoctorData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.updatePNCDoctorData(json("{\"investigation\":{\"laboratoryList\":[{}]}}"), + "auth")); + } + + @Test + void updatePNCDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(pncDoctorServiceImpl.updateBenPNCDiagnosis(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + Map drugResult = new HashMap<>(); + drugResult.put("count", 1); + drugResult.put("prescribedDrugIDs", new ArrayList()); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult); + when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, () -> service.updatePNCDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void updatePNCDoctorData_failsWhenASectionCouldNotBeUpdated() throws Exception { + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(0); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + + assertThrows(RuntimeException.class, () -> service.updatePNCDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void updatePNCDoctorData_booksTheSpecialistSlotBeforeRaisingATeleconsultationRequest() throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(1); + when(teleConsultationServiceImpl.createTCRequest(any())).thenReturn(1); + when(commonDoctorServiceImpl.updateDocFindings(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(pncDoctorServiceImpl.updateBenPNCDiagnosis(any())).thenReturn(1); + when(commonNurseServiceImpl.saveBenInvestigation(any())).thenReturn(1L); + Map drugResult = new HashMap<>(); + drugResult.put("count", 1); + drugResult.put("prescribedDrugIDs", Collections.singletonList(9L)); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult); + when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.updatePNCDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + } + + @Test + void updatePNCDoctorData_failsWhenTheSpecialistSlotCouldNotBeBooked() throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(0); + + assertThrows(RuntimeException.class, + () -> service.updatePNCDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + } + } } diff --git a/src/test/java/com/iemr/mmu/service/quickConsultation/QuickConsultationFlowTest.java b/src/test/java/com/iemr/mmu/service/quickConsultation/QuickConsultationFlowTest.java new file mode 100644 index 00000000..f244c131 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/quickConsultation/QuickConsultationFlowTest.java @@ -0,0 +1,439 @@ +/* +* 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.mmu.service.quickConsultation; + +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.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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.benFlowStatus.BeneficiaryFlowStatus; +import com.iemr.mmu.data.quickConsultation.BenChiefComplaint; +import com.iemr.mmu.data.quickConsultation.BenClinicalObservations; +import com.iemr.mmu.data.quickConsultation.ExternalLabTestOrder; +import com.iemr.mmu.data.quickConsultation.PrescriptionDetail; +import com.iemr.mmu.repo.benFlowStatus.BeneficiaryFlowStatusRepo; +import com.iemr.mmu.repo.nurse.BenPhysicalVitalRepo; +import com.iemr.mmu.repo.quickConsultation.BenChiefComplaintRepo; +import com.iemr.mmu.repo.quickConsultation.BenClinicalObservationsRepo; +import com.iemr.mmu.repo.quickConsultation.ExternalTestOrderRepo; +import com.iemr.mmu.repo.quickConsultation.PrescriptionDetailRepo; +import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonDoctorServiceImpl; +import com.iemr.mmu.service.common.transaction.CommonNurseServiceImpl; +import com.iemr.mmu.service.generalOPD.GeneralOPDDoctorServiceImpl; +import com.iemr.mmu.service.labtechnician.LabTechnicianServiceImpl; +import com.iemr.mmu.service.tele_consultation.TeleConsultationServiceImpl; + +/** + * Covers the quick-consultation flows end to end - the nurse visit, the + * doctor's first save and the doctor's later update - alongside the narrower + * per-method checks in {@link QuickConsultationServiceImplTest}. + */ +class QuickConsultationFlowTest { + + @Mock + private BenChiefComplaintRepo benChiefComplaintRepo; + @Mock + private BenClinicalObservationsRepo benClinicalObservationsRepo; + @Mock + private PrescriptionDetailRepo prescriptionDetailRepo; + @Mock + private ExternalTestOrderRepo externalTestOrderRepo; + @Mock + private CommonNurseServiceImpl commonNurseServiceImpl; + @Mock + private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl; + @Mock + private LabTechnicianServiceImpl labTechnicianServiceImpl; + @Mock + private CommonDoctorServiceImpl commonDoctorServiceImpl; + @Mock + private GeneralOPDDoctorServiceImpl generalOPDDoctorServiceImpl; + @Mock + private TeleConsultationServiceImpl teleConsultationServiceImpl; + @Mock + private BenPhysicalVitalRepo benPhysicalVitalRepo; + @Mock + private BeneficiaryFlowStatusRepo beneficiaryFlowStatusRepo; + + @InjectMocks + private QuickConsultationServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + private static String nurseRequest() { + return "{\"benFlowID\":3,\"vanID\":1,\"sessionID\":1," + + "\"visitDetails\":{\"beneficiaryRegID\":1,\"visitReason\":\"New Chief Complaint\"," + + "\"visitCategory\":\"General OPD (QC)\"},\"vitalsDetails\":{}}"; + } + + @Nested + @DisplayName("the nurse visit") + class NurseVisit { + + @Test + void quickConsultNurseDataInsert_savesTheVisitAndVitalsAndAdvancesTheFlow() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(1L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalVitalDetails(any())).thenReturn(1L); + + assertEquals(1, service.quickConsultNurseDataInsert(json(nurseRequest()))); + verify(commonBenStatusFlowServiceImpl).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), anyLong(), + anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any()); + } + + @Test + void quickConsultNurseDataInsert_reportsAVisitThatWasAlreadyCreatedRecently() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(1); + + assertEquals(3, service.quickConsultNurseDataInsert(json(nurseRequest()))); + } + + @Test + void quickConsultNurseDataInsert_skipsTheVisitWhenTheNurseAlreadySavedThisFlow() throws Exception { + when(beneficiaryFlowStatusRepo.checkExistData(any(), any())).thenReturn(new BeneficiaryFlowStatus()); + + assertEquals(0, service.quickConsultNurseDataInsert(json(nurseRequest()))); + verify(commonNurseServiceImpl, never()).saveBeneficiaryVisitDetails(any()); + } + + @Test + void quickConsultNurseDataInsert_leavesTheFlowUntouchedWhenTheVitalsFailToSave() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(5L); + when(commonNurseServiceImpl.generateVisitCode(anyLong(), any(), any())).thenReturn(6L); + when(commonNurseServiceImpl.saveBeneficiaryPhysicalAnthropometryDetails(any())).thenReturn(null); + + assertEquals(0, service.quickConsultNurseDataInsert(json(nurseRequest()))); + verify(commonBenStatusFlowServiceImpl, never()).updateBenFlowNurseAfterNurseActivity(any(), anyLong(), + anyLong(), anyString(), anyString(), any(), any(), any(), any(), any(), anyLong(), any()); + } + + @Test + void quickConsultNurseDataInsert_leavesTheFlowUntouchedWhenTheVisitWasNotCreated() throws Exception { + when(commonNurseServiceImpl.getMaxCurrentdate(any(), any(), any())).thenReturn(0); + when(commonNurseServiceImpl.saveBeneficiaryVisitDetails(any())).thenReturn(0L); + + assertEquals(0, service.quickConsultNurseDataInsert(json(nurseRequest()))); + } + + @Test + void quickConsultNurseDataInsert_ignoresARequestWithoutVisitDetails() throws Exception { + assertEquals(0, service.quickConsultNurseDataInsert(json("{}"))); + assertEquals(0, service.quickConsultNurseDataInsert(null)); + } + } + + @Nested + @DisplayName("the individual saves") + class Saves { + + @Test + void saveBeneficiaryChiefComplaint_storesEveryComplaintAndStampsItsVanSerial() { + BenChiefComplaint stored = new BenChiefComplaint(); + stored.setBenChiefComplaintID(9L); + when(benChiefComplaintRepo.saveAll(any())).thenReturn(Collections.singletonList(stored)); + + String request = "{\"chiefComplaintList\":[{\"chiefComplaintID\":1,\"chiefComplaint\":\"Fever\"}]}"; + + assertEquals(1L, service.saveBeneficiaryChiefComplaint(json(request))); + verify(benChiefComplaintRepo).updateVanSerialNo(9L); + } + + @Test + void saveBeneficiaryChiefComplaint_succeedsWhenNoComplaintWasEntered() { + assertEquals(1L, service.saveBeneficiaryChiefComplaint(json("{}"))); + verify(benChiefComplaintRepo, never()).saveAll(any()); + } + + @Test + void saveBeneficiaryChiefComplaint_reportsFailureWhenNotEveryComplaintWasStored() { + when(benChiefComplaintRepo.saveAll(any())).thenReturn(new ArrayList<>()); + + String request = "{\"chiefComplaintList\":[{\"chiefComplaintID\":1,\"chiefComplaint\":\"Fever\"}]}"; + + assertNull(service.saveBeneficiaryChiefComplaint(json(request))); + } + + @Test + void saveBeneficiaryClinicalObservations_attachesTheSnomedCodesOfTheSymptoms() throws Exception { + when(commonDoctorServiceImpl.getSnomedCTcode("Fever")) + .thenReturn(new String[] { "111", "Fever" }); + BenClinicalObservations stored = new BenClinicalObservations(); + stored.setClinicalObservationID(4L); + when(benClinicalObservationsRepo.save(any())).thenReturn(stored); + + assertEquals(4L, service.saveBeneficiaryClinicalObservations(json("{\"otherSymptoms\":\"Fever\"}"))); + } + + @Test + void saveBeneficiaryClinicalObservations_reportsNothingWhenTheRowWasNotStored() throws Exception { + BenClinicalObservations stored = new BenClinicalObservations(); + stored.setClinicalObservationID(0L); + when(benClinicalObservationsRepo.save(any())).thenReturn(stored); + + assertNull(service.saveBeneficiaryClinicalObservations(json("{}"))); + } + + @Test + void saveBenPrescriptionForANC_returnsTheStoredPrescriptionId() { + PrescriptionDetail prescription = new PrescriptionDetail(); + PrescriptionDetail stored = new PrescriptionDetail(); + stored.setPrescriptionID(7L); + when(prescriptionDetailRepo.save(prescription)).thenReturn(stored); + assertEquals(7L, service.saveBenPrescriptionForANC(prescription)); + + stored.setPrescriptionID(0L); + assertNull(service.saveBenPrescriptionForANC(prescription)); + } + + @Test + void saveBeneficiaryExternalLabTestOrderDetails_returnsTheStoredOrderId() { + ExternalLabTestOrder stored = new ExternalLabTestOrder(); + // The id is assigned by the database, so it is set through the field. + org.springframework.test.util.ReflectionTestUtils.setField(stored, "externalTestOrderID", 8L); + when(externalTestOrderRepo.save(any())).thenReturn(stored); + assertEquals(8L, service.saveBeneficiaryExternalLabTestOrderDetails(json("{}"))); + + org.springframework.test.util.ReflectionTestUtils.setField(stored, "externalTestOrderID", 0L); + assertNull(service.saveBeneficiaryExternalLabTestOrderDetails(json("{}"))); + } + + @Test + void updateBeneficiaryClinicalObservations_delegatesToTheDoctorService() throws Exception { + when(commonDoctorServiceImpl.getSnomedCTcode(any())).thenReturn(new String[] { "111", "Fever" }); + when(commonDoctorServiceImpl.updateBenClinicalObservations(any())).thenReturn(1); + + assertEquals(1, service.updateBeneficiaryClinicalObservations(json("{\"otherSymptoms\":\"Fever\"}"))); + } + } + + @Nested + @DisplayName("the doctor's save and update") + class DoctorData { + + private String doctorRequest(String extra) { + return "{\"beneficiaryRegID\":1,\"benVisitID\":2,\"visitCode\":3,\"providerServiceMapID\":4," + + "\"createdBy\":\"doctor\",\"prescriptionID\":7," + + "\"chiefComplaintList\":[{\"chiefComplaintID\":1,\"chiefComplaint\":\"Fever\"}]," + + "\"labTestOrders\":[{\"testID\":1}],\"prescription\":[{\"drugID\":1}]," + + "\"rbsTestResult\":\"90\",\"refer\":{}" + extra + "}"; + } + + private String teleconsultationBlock() { + return ",\"serviceID\":4,\"tcRequest\":{\"userID\":5,\"allocationDate\":\"2024-01-01\"," + + "\"fromTime\":\"10:00:00\",\"toTime\":\"10:30:00\"}"; + } + + private Map drugResult() { + Map result = new HashMap<>(); + result.put("count", 1); + result.put("prescribedDrugIDs", Collections.singletonList(9L)); + return result; + } + + @BeforeEach + void stubTheCommonSaves() throws Exception { + BenChiefComplaint storedComplaint = new BenChiefComplaint(); + storedComplaint.setBenChiefComplaintID(9L); + when(benChiefComplaintRepo.saveAll(any())).thenReturn(Collections.singletonList(storedComplaint)); + when(commonDoctorServiceImpl.getSnomedCTcode(any())).thenReturn(new String[] { "111", "Fever" }); + when(commonNurseServiceImpl.saveBenPrescribedDrugsList(any())).thenReturn(drugResult()); + when(commonNurseServiceImpl.saveBeneficiaryLabTestOrderDetails(any(), any())).thenReturn(1L); + when(benPhysicalVitalRepo.updatePhysicalVitalDetailsQCDoctor(any(), any(), any(), any())).thenReturn(1); + when(commonDoctorServiceImpl.saveBenReferDetails(any())).thenReturn(1L); + when(commonDoctorServiceImpl.updateBenReferDetails(any())).thenReturn(1L); + } + + @Test + void quickConsultDoctorDataInsert_savesEverySectionAndAdvancesTheFlow() throws Exception { + BenClinicalObservations storedObservation = new BenClinicalObservations(); + storedObservation.setClinicalObservationID(4L); + when(benClinicalObservationsRepo.save(any())).thenReturn(storedObservation); + when(commonNurseServiceImpl.saveBeneficiaryPrescription(any())).thenReturn(7L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1, service.quickConsultDoctorDataInsert(json(doctorRequest("")), "auth")); + } + + @Test + void quickConsultDoctorDataInsert_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + BenClinicalObservations storedObservation = new BenClinicalObservations(); + storedObservation.setClinicalObservationID(4L); + when(benClinicalObservationsRepo.save(any())).thenReturn(storedObservation); + when(commonNurseServiceImpl.saveBeneficiaryPrescription(any())).thenReturn(7L); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, + () -> service.quickConsultDoctorDataInsert(json(doctorRequest("")), "auth")); + } + + @Test + void quickConsultDoctorDataInsert_failsWhenASectionCouldNotBeSaved() throws Exception { + BenClinicalObservations storedObservation = new BenClinicalObservations(); + storedObservation.setClinicalObservationID(0L); + when(benClinicalObservationsRepo.save(any())).thenReturn(storedObservation); + when(commonNurseServiceImpl.saveBeneficiaryPrescription(any())).thenReturn(7L); + + assertThrows(RuntimeException.class, + () -> service.quickConsultDoctorDataInsert(json(doctorRequest("")), "auth")); + } + + @Test + void quickConsultDoctorDataInsert_booksTheSpecialistSlotBeforeRaisingATeleconsultationRequest() + throws Exception { + BenClinicalObservations storedObservation = new BenClinicalObservations(); + storedObservation.setClinicalObservationID(4L); + when(benClinicalObservationsRepo.save(any())).thenReturn(storedObservation); + when(commonNurseServiceImpl.saveBeneficiaryPrescription(any())).thenReturn(7L); + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(1); + when(teleConsultationServiceImpl.createTCRequest(any())).thenReturn(1); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataSave(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1, + service.quickConsultDoctorDataInsert(json(doctorRequest(teleconsultationBlock())), "auth")); + verify(teleConsultationServiceImpl).createTCRequest(any()); + } + + @Test + void quickConsultDoctorDataInsert_failsWhenTheSpecialistSlotCouldNotBeBooked() { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(0); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> service.quickConsultDoctorDataInsert(json(doctorRequest(teleconsultationBlock())), "auth")); + assertEquals("Error while booking slot.", thrown.getMessage()); + } + + @Test + void updateGeneralOPDQCDoctorData_updatesEverySectionAndAdvancesTheFlow() throws Exception { + when(commonDoctorServiceImpl.updateBenClinicalObservations(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, service.updateGeneralOPDQCDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void updateGeneralOPDQCDoctorData_failsWhenTheBeneficiaryFlowCouldNotBeAdvanced() throws Exception { + when(commonDoctorServiceImpl.updateBenClinicalObservations(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(0); + + assertThrows(RuntimeException.class, + () -> service.updateGeneralOPDQCDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void updateGeneralOPDQCDoctorData_failsWhenASectionCouldNotBeUpdated() throws Exception { + when(commonDoctorServiceImpl.updateBenClinicalObservations(any())).thenReturn(0); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + + assertThrows(RuntimeException.class, + () -> service.updateGeneralOPDQCDoctorData(json(doctorRequest("")), "auth")); + } + + @Test + void updateGeneralOPDQCDoctorData_treatsEveryAbsentSectionAsAlreadyDone() throws Exception { + when(commonDoctorServiceImpl.updateBenClinicalObservations(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + + String minimal = "{\"beneficiaryRegID\":1,\"prescriptionID\":7," + + "\"chiefComplaintList\":[{\"chiefComplaintID\":1,\"chiefComplaint\":\"Fever\"}]}"; + + assertEquals(1L, service.updateGeneralOPDQCDoctorData(json(minimal), "auth")); + } + + @Test + void updateGeneralOPDQCDoctorData_booksTheSpecialistSlotBeforeRaisingATeleconsultationRequest() + throws Exception { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(1); + when(teleConsultationServiceImpl.createTCRequest(any())).thenReturn(1); + when(commonDoctorServiceImpl.updateBenClinicalObservations(any())).thenReturn(1); + when(commonNurseServiceImpl.updatePrescription(any())).thenReturn(1); + when(commonDoctorServiceImpl.updateBenFlowtableAfterDocDataUpdate(any(), any(), any(), any(), any())) + .thenReturn(1); + + assertEquals(1L, + service.updateGeneralOPDQCDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + } + + @Test + void updateGeneralOPDQCDoctorData_failsWhenTheSpecialistSlotCouldNotBeBooked() { + when(commonDoctorServiceImpl.callTmForSpecialistSlotBook(any(), anyString())).thenReturn(0); + + assertThrows(RuntimeException.class, () -> service + .updateGeneralOPDQCDoctorData(json(doctorRequest(teleconsultationBlock())), "auth")); + } + } + + @Nested + @DisplayName("the case sheet reads") + class Reads { + + @Test + void getBenQuickConsultNurseData_carriesTheVitals() { + when(commonNurseServiceImpl.getBeneficiaryPhysicalAnthropometryDetails(1L, 2L)).thenReturn("a"); + when(commonNurseServiceImpl.getBeneficiaryPhysicalVitalDetails(1L, 2L)).thenReturn("v"); + + assertTrue(service.getBenQuickConsultNurseData(1L, 2L).contains("vitals")); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/registrar/RegistrarServiceImplTest.java b/src/test/java/com/iemr/mmu/service/registrar/RegistrarServiceImplTest.java new file mode 100644 index 00000000..958b1687 --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/registrar/RegistrarServiceImplTest.java @@ -0,0 +1,612 @@ +/* +* 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.mmu.service.registrar; + +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.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +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.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +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.RestTemplate; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.mmu.data.registrar.BeneficiaryData; +import com.iemr.mmu.data.registrar.BeneficiaryDemographicAdditional; +import com.iemr.mmu.data.registrar.BeneficiaryDemographicData; +import com.iemr.mmu.data.registrar.BeneficiaryImage; +import com.iemr.mmu.data.registrar.BeneficiaryPhoneMapping; +import com.iemr.mmu.data.registrar.V_BenAdvanceSearch; +import com.iemr.mmu.repo.registrar.BeneficiaryDemographicAdditionalRepo; +import com.iemr.mmu.repo.registrar.BeneficiaryImageRepo; +import com.iemr.mmu.repo.registrar.RegistrarRepoBenData; +import com.iemr.mmu.repo.registrar.RegistrarRepoBenDemoData; +import com.iemr.mmu.repo.registrar.RegistrarRepoBenGovIdMapping; +import com.iemr.mmu.repo.registrar.RegistrarRepoBenPhoneMapData; +import com.iemr.mmu.repo.registrar.RegistrarRepoBeneficiaryDetails; +import com.iemr.mmu.repo.registrar.ReistrarRepoBenSearch; +import com.iemr.mmu.service.benFlowStatus.CommonBenStatusFlowServiceImpl; +import com.iemr.mmu.utils.CookieUtil; + +class RegistrarServiceImplTest { + + @Mock + private RegistrarRepoBenData registrarRepoBenData; + @Mock + private RegistrarRepoBenDemoData registrarRepoBenDemoData; + @Mock + private RegistrarRepoBenPhoneMapData registrarRepoBenPhoneMapData; + @Mock + private RegistrarRepoBenGovIdMapping registrarRepoBenGovIdMapping; + @Mock + private ReistrarRepoBenSearch reistrarRepoBenSearch; + @Mock + private BeneficiaryDemographicAdditionalRepo beneficiaryDemographicAdditionalRepo; + @Mock + private RegistrarRepoBeneficiaryDetails registrarRepoBeneficiaryDetails; + @Mock + private BeneficiaryImageRepo beneficiaryImageRepo; + @Mock + private CommonBenStatusFlowServiceImpl commonBenStatusFlowServiceImpl; + @Mock + private CookieUtil cookieUtil; + + @InjectMocks + private RegistrarServiceImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(service, "registrationUrl", "http://registry/register"); + ReflectionTestUtils.setField(service, "registrarQuickSearchByIdUrl", "http://registry/searchById"); + ReflectionTestUtils.setField(service, "registrarQuickSearchByPhoneNoUrl", "http://registry/searchByPhone"); + ReflectionTestUtils.setField(service, "beneficiaryEditUrl", "http://registry/edit"); + ReflectionTestUtils.setField(service, "registrarAdvanceSearchUrl", "http://registry/advanceSearch"); + } + + private static JsonObject json(String raw) { + return JsonParser.parseString(raw).getAsJsonObject(); + } + + /** A registration request with every optional field the mappers look for. */ + private static String fullBeneficiary() { + return "{\"firstName\":\"Asha\",\"lastName\":\"Devi\",\"gender\":2," + + "\"dob\":\"1990-05-04T00:00:00.000Z\",\"maritalStatus\":1,\"createdBy\":\"registrar\"," + + "\"fatherName\":\"Ram\",\"husbandName\":\"Shyam\",\"aadharNo\":\"1234\"," + + "\"beneficiaryRegID\":1,\"modifiedBy\":\"registrar\",\"countryID\":1,\"stateID\":2," + + "\"districtID\":3,\"blockID\":4,\"servicePointID\":5,\"villageID\":6,\"community\":1," + + "\"religion\":2,\"occupation\":3,\"educationQualification\":4,\"income\":5," + + "\"benDemographicsID\":7,\"phoneNo\":\"9999999999\",\"benPhMapID\":8," + + "\"literacyStatus\":\"Literate\",\"motherName\":\"Sita\",\"emailID\":\"a@b.c\"," + + "\"bankName\":\"Bank\",\"branchName\":\"Branch\",\"IFSCCode\":\"IFSC1\"," + + "\"accountNumber\":\"123\",\"ageAtMarriage\":20,\"age\":34,\"benDemoAdditionalID\":9," + + "\"image\":\"base64\",\"benImageID\":10}"; + } + + @Nested + @DisplayName("creating a beneficiary") + class Create { + + @Test + void createBeneficiary_storesTheMappedBeneficiary() { + BeneficiaryData stored = new BeneficiaryData(); + when(registrarRepoBenData.save(any())).thenReturn(stored); + assertEquals(stored, service.createBeneficiary(json(fullBeneficiary()))); + } + + @Test + void createBeneficiaryDemographic_returnsTheStoredDemographicId() { + BeneficiaryDemographicData stored = new BeneficiaryDemographicData(); + stored.setBenDemographicsID(5L); + when(registrarRepoBenDemoData.save(any())).thenReturn(stored); + assertEquals(5L, service.createBeneficiaryDemographic(json(fullBeneficiary()), 1L)); + + when(registrarRepoBenDemoData.save(any())).thenReturn(null); + assertNull(service.createBeneficiaryDemographic(json("{}"), 1L)); + } + + @Test + void createBeneficiaryDemographicAdditional_returnsTheStoredId() { + BeneficiaryDemographicAdditional stored = new BeneficiaryDemographicAdditional(); + stored.setBenDemoAdditionalID(6L); + when(beneficiaryDemographicAdditionalRepo.save(any())).thenReturn(stored); + assertEquals(6L, service.createBeneficiaryDemographicAdditional(json(fullBeneficiary()), 1L)); + + when(beneficiaryDemographicAdditionalRepo.save(any())).thenReturn(null); + assertNull(service.createBeneficiaryDemographicAdditional(json("{}"), 1L)); + } + + @Test + void createBeneficiaryImage_returnsTheStoredBeneficiaryId() { + BeneficiaryImage stored = new BeneficiaryImage(); + stored.setBeneficiaryRegID(1L); + when(beneficiaryImageRepo.save(any())).thenReturn(stored); + + assertEquals(1L, service.createBeneficiaryImage( + json("{\"image\":\"base64\",\"createdBy\":\"registrar\"}"), 1L)); + + when(beneficiaryImageRepo.save(any())).thenReturn(null); + assertNull(service.createBeneficiaryImage(json("{\"image\":null,\"createdBy\":null}"), 1L)); + } + + @Test + void createBeneficiaryPhoneMapping_returnsTheStoredMappingId() { + BeneficiaryPhoneMapping stored = new BeneficiaryPhoneMapping(); + stored.setBenPhMapID(7L); + when(registrarRepoBenPhoneMapData.save(any())).thenReturn(stored); + assertEquals(7L, service.createBeneficiaryPhoneMapping(json(fullBeneficiary()), 1L)); + + when(registrarRepoBenPhoneMapData.save(any())).thenReturn(null); + assertNull(service.createBeneficiaryPhoneMapping(json("{}"), 1L)); + } + + @Test + void createBenGovIdMapping_reportsHowManyIdentitiesWereStored() { + when(registrarRepoBenGovIdMapping.saveAll(any())).thenAnswer(invocation -> { + List saved = invocation.getArgument(0); + return new ArrayList<>(saved); + }); + + String request = "{\"createdBy\":\"registrar\",\"govID\":[{\"type\":1,\"value\":\"1234\"},{}]}"; + assertEquals(2, service.createBenGovIdMapping(json(request), 1L)); + } + } + + @Nested + @DisplayName("mapping the request onto the stored beneficiary") + class Mapping { + + @Test + void getBenOBJ_readsEveryOptionalFieldOfTheRequest() { + BeneficiaryData mapped = service.getBenOBJ(json(fullBeneficiary())); + + assertEquals("Asha", mapped.getFirstName()); + assertEquals("Devi", mapped.getLastName()); + assertEquals((short) 2, mapped.getGenderID()); + assertNotNull(mapped.getDob()); + assertEquals("Ram", mapped.getFatherName()); + assertEquals("Shyam", mapped.getSpouseName()); + assertEquals("1234", mapped.getAadharNo()); + assertEquals(1L, mapped.getBeneficiaryRegID()); + assertEquals("registrar", mapped.getModifiedBy()); + } + + @Test + void getBenOBJ_leavesTheDateOfBirthUnsetWhenItCannotBeParsed() { + assertNull(service.getBenOBJ(json("{\"dob\":\"not-a-date\"}")).getDob()); + } + + @Test + void getBenOBJ_toleratesAnEmptyRequest() { + assertNotNull(service.getBenOBJ(json("{}"))); + assertNull(service.getBenOBJ(json("{\"husbandName\":null,\"beneficiaryRegID\":null," + + "\"modifiedBy\":null}")).getSpouseName()); + } + + @Test + void getBenDemoOBJ_readsEveryOptionalFieldOfTheRequest() { + BeneficiaryDemographicData mapped = service.getBenDemoOBJ(json(fullBeneficiary()), 1L); + + assertEquals(1, mapped.getCountryID()); + assertEquals(6, mapped.getDistrictBranchID()); + assertEquals((short) 5, mapped.getIncomeStatusID()); + assertEquals(7L, mapped.getBenDemographicsID()); + } + + @Test + void getBenDemoOBJ_toleratesAnEmptyRequest() { + assertEquals(1L, service.getBenDemoOBJ(json("{}"), 1L).getBeneficiaryRegID()); + } + + @Test + void getBenPhoneOBJ_readsEveryOptionalFieldOfTheRequest() { + BeneficiaryPhoneMapping mapped = service.getBenPhoneOBJ(json(fullBeneficiary()), 1L); + + assertEquals("9999999999", mapped.getPhoneNo()); + assertEquals(8L, mapped.getBenPhMapID()); + assertEquals("registrar", mapped.getModifiedBy()); + } + + @Test + void getBenPhoneOBJ_toleratesAnEmptyRequest() { + assertEquals(1L, service.getBenPhoneOBJ(json("{}"), 1L).getBenificiaryRegID()); + } + } + + @Nested + @DisplayName("searching and work lists") + class Search { + + @Test + void getRegWorkList_serialisesTheStoredWorklist() { + when(registrarRepoBenData.getRegistrarWorkList(1)).thenReturn(new ArrayList<>()); + assertNotNull(service.getRegWorkList(1)); + } + + @Test + void getQuickSearchBenData_serialisesTheMatchingBeneficiaries() { + when(reistrarRepoBenSearch.getQuickSearch("BEN1")).thenReturn(new ArrayList<>()); + assertNotNull(service.getQuickSearchBenData("BEN1")); + } + + @Test + void getAdvanceSearchBenData_passesEveryProvidedCriterionToTheQuery() { + V_BenAdvanceSearch criteria = new V_BenAdvanceSearch(); + criteria.setBeneficiaryID("BEN1"); + criteria.setFirstName("Asha"); + criteria.setLastName("Devi"); + criteria.setFatherName("Ram"); + criteria.setPhoneNo("9999999999"); + criteria.setAadharNo("1234"); + criteria.setGovtIdentityNo("GOV1"); + criteria.setStateID(2); + criteria.setDistrictID(3); + when(reistrarRepoBenSearch.getAdvanceBenSearchList("BEN1", "Asha", "Devi", "9999999999", "1234", "GOV1", + "2", "3")).thenReturn(new ArrayList<>()); + + assertNotNull(service.getAdvanceSearchBenData(criteria)); + } + + @Test + void getAdvanceSearchBenData_fallsBackToWildcardsWhenNoCriterionWasGiven() { + when(reistrarRepoBenSearch.getAdvanceBenSearchList("%%", "", "", "%%", "%%", "%%", "%%", "%%")) + .thenReturn(new ArrayList<>()); + + assertNotNull(service.getAdvanceSearchBenData(new V_BenAdvanceSearch())); + } + + @Test + void getAdvanceSearchBenData_returnsNothingWhenTheQueryFails() { + when(reistrarRepoBenSearch.getAdvanceBenSearchList(anyString(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyString(), anyString())).thenThrow(new RuntimeException("db down")); + + assertEquals("", service.getAdvanceSearchBenData(new V_BenAdvanceSearch())); + } + } + + @Nested + @DisplayName("reading a beneficiary") + class Reads { + + /** One beneficiary-details row, wide enough for the details mapper. */ + private List detailRows(Boolean isGovType) { + Object[] row = new Object[40]; + row[24] = Short.valueOf((short) 1); + row[25] = "1234"; + row[26] = isGovType; + List rows = new ArrayList<>(); + rows.add(row); + return rows; + } + + @Test + void getBeneficiaryDetails_separatesGovernmentIdentitiesFromTheOthers() { + when(registrarRepoBeneficiaryDetails.getBeneficiaryDetails(1L)).thenReturn(detailRows(Boolean.TRUE)); + when(beneficiaryImageRepo.getBenImage(1L)).thenReturn("base64"); + + assertTrue(service.getBeneficiaryDetails(1L).contains("1234")); + } + + @Test + void getBeneficiaryDetails_recordsANonGovernmentIdentitySeparately() { + when(registrarRepoBeneficiaryDetails.getBeneficiaryDetails(1L)).thenReturn(detailRows(Boolean.FALSE)); + + assertNotNull(service.getBeneficiaryDetails(1L)); + } + + @Test + void getBeneficiaryDetails_toleratesARowWithoutAnIdentityType() { + when(registrarRepoBeneficiaryDetails.getBeneficiaryDetails(1L)).thenReturn(detailRows(null)); + + assertNotNull(service.getBeneficiaryDetails(1L)); + } + + @Test + void getBeneficiaryDetails_returnsNothingForAnUnknownBeneficiary() { + when(registrarRepoBeneficiaryDetails.getBeneficiaryDetails(1L)).thenReturn(new ArrayList<>()); + assertNull(service.getBeneficiaryDetails(1L)); + } + + @Test + void getBenImage_returnsTheStoredImage() { + when(beneficiaryImageRepo.getBenImage(1L)).thenReturn("base64"); + assertTrue(service.getBenImage(1L).contains("base64")); + + when(beneficiaryImageRepo.getBenImage(2L)).thenReturn(null); + assertEquals("{}", service.getBenImage(2L)); + } + + @Test + void getBeneficiaryPersonalDetails_namesTheGenderAndAttachesTheServicePoint() { + Object[] detailRow = new Object[30]; + detailRow[0] = 1L; + List benRows = new ArrayList<>(); + benRows.add(detailRow); + when(registrarRepoBenData.getBenDetailsByRegID(1L)).thenReturn(benRows); + + Object[] demoRow = new Object[10]; + demoRow[2] = "PHC Alpha"; + List demoRows = new ArrayList<>(); + demoRows.add(demoRow); + when(registrarRepoBenDemoData.getBeneficiaryDemographicData(1L)).thenReturn(demoRows); + + BeneficiaryData details = service.getBeneficiaryPersonalDetails(1L); + + assertNotNull(details); + assertEquals("PHC Alpha", details.getServicePointName()); + } + + @Test + void getBeneficiaryPersonalDetails_returnsNothingForAnUnknownBeneficiary() { + when(registrarRepoBenData.getBenDetailsByRegID(1L)).thenReturn(new ArrayList<>()); + when(registrarRepoBenDemoData.getBeneficiaryDemographicData(1L)).thenReturn(new ArrayList<>()); + + assertNull(service.getBeneficiaryPersonalDetails(1L)); + } + } + + @Nested + @DisplayName("updating a beneficiary") + class Updates { + + @Test + void updateBeneficiary_updatesTheStoredBeneficiary() { + when(registrarRepoBenData.updateBeneficiaryData(any(), any(), any(), any(), any(), any(), any(), any(), + any(), any())).thenReturn(1); + assertEquals(1, service.updateBeneficiary(json(fullBeneficiary()))); + } + + @Test + void updateBeneficiaryDemographic_updatesTheStoredDemographics() { + when(registrarRepoBenDemoData.updateBendemographicData(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any())).thenReturn(1); + assertEquals(1, service.updateBeneficiaryDemographic(json(fullBeneficiary()), 1L)); + } + + @Test + void updateBeneficiaryPhoneMapping_updatesTheStoredPhoneNumber() { + when(registrarRepoBenPhoneMapData.updateBenPhoneMap(any(), any(), any())).thenReturn(1); + assertEquals(1, service.updateBeneficiaryPhoneMapping(json(fullBeneficiary()), 1L)); + } + + @Test + void updateBenGovIdMapping_replacesTheStoredIdentities() { + when(registrarRepoBenGovIdMapping.saveAll(any())).thenReturn(new ArrayList<>()); + + assertEquals(0, service.updateBenGovIdMapping(json("{\"govID\":[]}"), 1L)); + verify(registrarRepoBenGovIdMapping).deletePreviousGovMapID(1L); + } + + @Test + void updateBeneficiaryDemographicAdditional_updatesTheStoredRowWhenOneExists() { + when(beneficiaryDemographicAdditionalRepo.getBeneficiaryDemographicAdditional(1L)) + .thenReturn(new BeneficiaryDemographicAdditional()); + when(beneficiaryDemographicAdditionalRepo.updateBeneficiaryDemographicAdditional(any(), any(), any(), + any(), any(), any(), any(), any(), any())).thenReturn(1); + + assertEquals(1, service.updateBeneficiaryDemographicAdditional(json(fullBeneficiary()), 1L)); + } + + @Test + void updateBeneficiaryDemographicAdditional_insertsAFreshRowWhenNoneIsStoredYet() { + BeneficiaryDemographicAdditional stored = new BeneficiaryDemographicAdditional(); + stored.setBenDemoAdditionalID(5L); + when(beneficiaryDemographicAdditionalRepo.getBeneficiaryDemographicAdditional(1L)).thenReturn(null); + when(beneficiaryDemographicAdditionalRepo.save(any())).thenReturn(stored); + + assertEquals(1, service.updateBeneficiaryDemographicAdditional(json(fullBeneficiary()), 1L)); + } + + @Test + void updateBeneficiaryDemographicAdditional_reportsFailureWhenTheFreshRowWasNotStored() { + BeneficiaryDemographicAdditional stored = new BeneficiaryDemographicAdditional(); + stored.setBenDemoAdditionalID(0L); + when(beneficiaryDemographicAdditionalRepo.getBeneficiaryDemographicAdditional(1L)).thenReturn(null); + when(beneficiaryDemographicAdditionalRepo.save(any())).thenReturn(stored); + + assertEquals(0, service.updateBeneficiaryDemographicAdditional(json(fullBeneficiary()), 1L)); + } + + @Test + void updateBeneficiaryImage_updatesTheStoredImageWhenOneExists() { + when(beneficiaryImageRepo.findBenImage(1L)).thenReturn(1L); + when(beneficiaryImageRepo.updateBeneficiaryImage(anyString(), any(), anyLong())).thenReturn(1); + + assertEquals(1, service.updateBeneficiaryImage(json(fullBeneficiary()), 1L)); + } + + @Test + void updateBeneficiaryImage_insertsAFreshImageWhenNoneIsStoredYet() { + BeneficiaryImage stored = new BeneficiaryImage(); + stored.setBenImageID(5L); + when(beneficiaryImageRepo.findBenImage(1L)).thenReturn(null); + when(beneficiaryImageRepo.save(any())).thenReturn(stored); + + assertEquals(1, service.updateBeneficiaryImage(json(fullBeneficiary()), 1L)); + } + + @Test + void updateBeneficiaryImage_reportsFailureWhenTheFreshImageWasNotStored() { + BeneficiaryImage stored = new BeneficiaryImage(); + stored.setBenImageID(0L); + when(beneficiaryImageRepo.findBenImage(1L)).thenReturn(null); + when(beneficiaryImageRepo.save(any())).thenReturn(stored); + + assertEquals(0, service.updateBeneficiaryImage(json(fullBeneficiary()), 1L)); + } + + @Test + void updateBeneficiaryImage_succeedsWithoutAnImageToStore() { + assertEquals(1, service.updateBeneficiaryImage(json("{}"), 1L)); + } + } + + @Nested + @DisplayName("talking to the central identity service") + class CentralIdentityService { + + @Test + void registerBeneficiary_createsTheBeneficiaryFlowRecordForANewRegistration() throws Exception { + String body = "{\"data\":{\"beneficiaryRegID\":1,\"beneficiaryID\":2}}"; + when(commonBenStatusFlowServiceImpl.createBenFlowRecord(anyString(), eq(1L), eq(2L))).thenReturn(1); + + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(body, HttpStatus.OK)))) { + + assertTrue(service.registerBeneficiary("{}", "auth", "token").contains("successfully registered")); + } + } + + @Test + void registerBeneficiary_reportsAnErrorWhenTheFlowRecordCouldNotBeCreated() throws Exception { + String body = "{\"data\":{\"beneficiaryRegID\":1,\"beneficiaryID\":2}}"; + when(commonBenStatusFlowServiceImpl.createBenFlowRecord(anyString(), eq(1L), eq(2L))).thenReturn(0); + + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(body, HttpStatus.OK)))) { + + assertTrue(service.registerBeneficiary("{}", "auth", "token").contains("contact administrator")); + } + } + + @Test + void registerBeneficiary_reportsTheGenericFailureWhenTheRegistryRejectsTheRequest() throws Exception { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(HttpStatus.BAD_REQUEST)))) { + + assertTrue(service.registerBeneficiary("{}", "auth", "token").contains("FAILURE")); + } + } + + @Test + void updateBeneficiary_passesTheBeneficiaryToTheNurseWhenTheRegistrarAskedForIt() throws Exception { + when(commonBenStatusFlowServiceImpl.createBenFlowRecord(anyString(), eq(null), eq(null))).thenReturn(1); + + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>("", HttpStatus.OK)))) { + + assertEquals(1, service.updateBeneficiary("{\"passToNurse\":true}", "auth", "token")); + } + } + + @Test + void updateBeneficiary_leavesTheBeneficiaryWithTheRegistrarByDefault() throws Exception { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>("", HttpStatus.OK)))) { + + assertEquals(1, service.updateBeneficiary("{\"passToNurse\":false}", "auth", "token")); + } + } + + @Test + void updateBeneficiary_returnsNothingWhenTheRegistryRejectsTheRequest() throws Exception { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(HttpStatus.BAD_REQUEST)))) { + + assertNull(service.updateBeneficiary("{}", "auth", "token")); + } + } + + @Test + void beneficiaryQuickSearch_searchesByBeneficiaryIdWhenOneWasGiven() { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>("byId", HttpStatus.OK)))) { + + assertEquals("byId", service.beneficiaryQuickSearch("{\"beneficiaryID\":\"BEN1\"}", "auth", "token")); + } + } + + @Test + void beneficiaryQuickSearch_searchesByPhoneNumberWhenNoIdWasGiven() { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>("byPhone", HttpStatus.OK)))) { + + assertEquals("byPhone", + service.beneficiaryQuickSearch("{\"phoneNo\":\"9999999999\"}", "auth", "token")); + } + } + + @Test + void beneficiaryQuickSearch_returnsNothingWhenNoCriterionWasGiven() { + try (MockedConstruction rest = mockConstruction(RestTemplate.class)) { + assertNull(service.beneficiaryQuickSearch("{}", "auth", "token")); + } + } + + @Test + void beneficiaryAdvanceSearch_returnsTheRegistryResponse() { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>("matches", HttpStatus.OK)))) { + + assertEquals("matches", service.beneficiaryAdvanceSearch("{}", "auth", "token")); + } + } + + @Test + void beneficiaryAdvanceSearch_returnsNothingWhenTheRegistryHasNoBody() { + try (MockedConstruction rest = mockConstruction(RestTemplate.class, + (mock, context) -> when(mock.exchange(anyString(), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(HttpStatus.OK)))) { + + assertNull(service.beneficiaryAdvanceSearch("{}", "auth", "token")); + } + } + + @Test + void searchAndSubmitBeneficiaryToNurse_createsTheBeneficiaryFlowRecord() throws Exception { + when(commonBenStatusFlowServiceImpl.createBenFlowRecord("{}", null, null)).thenReturn(1); + assertEquals(1, service.searchAndSubmitBeneficiaryToNurse("{}")); + } + } +} diff --git a/src/test/java/com/iemr/mmu/service/reports/ReportCheckPostImplTest.java b/src/test/java/com/iemr/mmu/service/reports/ReportCheckPostImplTest.java new file mode 100644 index 00000000..753a1e9e --- /dev/null +++ b/src/test/java/com/iemr/mmu/service/reports/ReportCheckPostImplTest.java @@ -0,0 +1,140 @@ +/* +* 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.mmu.service.reports; + +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.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.ArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.iemr.mmu.repo.reports.ReportMasterRepo; + +class ReportCheckPostImplTest { + + @Mock + private ReportMasterRepo reportMasterRepo; + + @InjectMocks + private ReportCheckPostImpl service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + // The output mapper's Gson builder is initialised by its constructor. + new com.iemr.mmu.utils.mapper.OutputMapper(); + } + + /** A report request for the given report, with every parameter the reports need. */ + private String request(int reportID, Integer vanID) { + return "{\"reportID\":" + reportID + ",\"fromDate\":\"2024-01-01T00:00:00.000\"," + + "\"toDate\":\"2024-01-31T00:00:00.000\",\"vanID\":" + vanID + ",\"providerServiceMapID\":4}"; + } + + /** One report row, wide enough for any of the report mappers. */ + private ArrayList oneRow() { + ArrayList rows = new ArrayList<>(); + rows.add(new Object[60]); + return rows; + } + + private void stubEveryReportQuery(ArrayList rows) { + when(reportMasterRepo.get_report_PatientAttended(any(), any(), anyInt(), any())).thenReturn(rows); + when(reportMasterRepo.get_report_TestConducted(any(), any(), anyInt(), any())).thenReturn(rows); + when(reportMasterRepo.get_report_LabTestResult(any(), any(), anyInt(), any())).thenReturn(rows); + when(reportMasterRepo.get_report_PatientInfo(any(), any(), anyInt(), any())).thenReturn(rows); + when(reportMasterRepo.get_report_SP_ChildrenCases(any(), any(), anyInt(), any())).thenReturn(rows); + when(reportMasterRepo.get_report_SP_ANC(any(), any(), anyInt(), any())).thenReturn(rows); + when(reportMasterRepo.get_report_SP_ANCHighRisk(any(), any(), anyInt(), any())).thenReturn(rows); + } + + @Test + @DisplayName("the report master list is served for a service") + void getReportMaster_servesTheReportsOfAService() throws Exception { + when(reportMasterRepo.findByServiceIDAndDeletedOrderByReportNameAsc(2, false)).thenReturn(new ArrayList<>()); + + assertEquals("[]", service.getReportMaster(2)); + } + + @ParameterizedTest(name = "report {0} is served from its own query") + @ValueSource(ints = { 1, 2, 5, 6, 7, 8, 9 }) + void reportHandler_servesEveryConfiguredReport(int reportID) throws Exception { + stubEveryReportQuery(oneRow()); + + assertTrue(service.reportHandler(request(reportID, 3)).startsWith("[")); + } + + @ParameterizedTest(name = "report {0} is served as an empty report when nothing matched") + @ValueSource(ints = { 1, 2, 5, 6, 7, 8, 9 }) + void reportHandler_servesAnEmptyReportWhenNothingMatched(int reportID) throws Exception { + stubEveryReportQuery(new ArrayList<>()); + + assertEquals("[]", service.reportHandler(request(reportID, 3))); + } + + @Test + @DisplayName("a van id of zero widens the report to every van") + void reportHandler_widensTheReportToEveryVanForAVanIdOfZero() throws Exception { + stubEveryReportQuery(new ArrayList<>()); + + assertEquals("[]", service.reportHandler(request(1, 0))); + verify(reportMasterRepo).get_report_PatientAttended(any(), any(), anyInt(), + org.mockito.ArgumentMatchers.isNull()); + } + + @Test + @DisplayName("an unknown report id is served as an empty response") + void reportHandler_servesAnEmptyResponseForAnUnknownReport() throws Exception { + assertEquals("", service.reportHandler(request(99, 3))); + } + + @Test + @DisplayName("a request without a report id is rejected") + void reportHandler_rejectsARequestWithoutAReportId() { + Exception thrown = assertThrows(Exception.class, () -> service.reportHandler("{}")); + assertEquals("Invalid/NULL report ID", thrown.getMessage()); + } + + @ParameterizedTest(name = "report {0} is rejected when a parameter is missing") + @ValueSource(ints = { 1, 2, 5, 6, 7, 8, 9 }) + void reportHandler_rejectsAReportWithAMissingParameter(int reportID) { + String incomplete = "{\"reportID\":" + reportID + ",\"providerServiceMapID\":4}"; + + Exception thrown = assertThrows(Exception.class, () -> service.reportHandler(incomplete)); + assertEquals("Some parameter/parameters is/are missing.", thrown.getMessage()); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/AESEncryption/AESEncryptionDecryptionTest.java b/src/test/java/com/iemr/mmu/utils/AESEncryption/AESEncryptionDecryptionTest.java new file mode 100644 index 00000000..479cf8fe --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/AESEncryption/AESEncryptionDecryptionTest.java @@ -0,0 +1,68 @@ +/* +* 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.mmu.utils.AESEncryption; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class AESEncryptionDecryptionTest { + + @Test + @DisplayName("an encrypted value decrypts back to the original") + void encryptThenDecrypt_returnsTheOriginalValue() throws Exception { + AESEncryptionDecryption cipher = new AESEncryptionDecryption(); + + String encrypted = cipher.encrypt("/mmu/reports/2024/report.pdf"); + + assertNotEquals("/mmu/reports/2024/report.pdf", encrypted); + assertEquals("/mmu/reports/2024/report.pdf", cipher.decrypt(encrypted)); + } + + @Test + @DisplayName("each encryption uses a fresh initialisation vector") + void encrypt_usesAFreshInitialisationVectorEachTime() throws Exception { + AESEncryptionDecryption cipher = new AESEncryptionDecryption(); + + assertNotEquals(cipher.encrypt("same value"), cipher.encrypt("same value")); + } + + @Test + @DisplayName("an explicitly set key is used for both directions") + void setKey_isUsedForBothDirections() throws Exception { + AESEncryptionDecryption cipher = new AESEncryptionDecryption(); + cipher.setKey("a-custom-key"); + + assertEquals("value", cipher.decrypt(cipher.encrypt("value"))); + } + + @Test + @DisplayName("a value that was not produced by this cipher cannot be decrypted") + void decrypt_failsForAValueThisCipherDidNotProduce() { + AESEncryptionDecryption cipher = new AESEncryptionDecryption(); + + assertThrows(Exception.class, () -> cipher.decrypt("bm90LWVuY3J5cHRlZC1hdC1hbGw=")); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/JwtSecurityUtilsTest.java b/src/test/java/com/iemr/mmu/utils/JwtSecurityUtilsTest.java new file mode 100644 index 00000000..fb7d9e8e --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/JwtSecurityUtilsTest.java @@ -0,0 +1,451 @@ +/* +* 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.mmu.utils; + +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.mmu.data.login.Users; +import com.iemr.mmu.repo.login.UserLoginRepo; +import com.iemr.mmu.utils.exception.IEMRException; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; + +class JwtSecurityUtilsTest { + + /** Long enough for HMAC-SHA; the filter and util derive the key from it. */ + private static final String SECRET = "a-very-long-jwt-signing-secret-for-mmu-api-tests-0123456789"; + + private static SecretKey signingKey() { + return Keys.hmacShaKeyFor(SECRET.getBytes()); + } + + private static String tokenWith(String userId, String jti) { + var builder = Jwts.builder().subject("nurse1").claim("userId", userId) + .expiration(new Date(System.currentTimeMillis() + 60_000)); + if (jti != null) { + builder.id(jti); + } + return builder.signWith(signingKey()).compact(); + } + + @Nested + @DisplayName("JwtUtil") + class JwtUtilTests { + + @Mock + private TokenDenylist tokenDenylist; + + @InjectMocks + private JwtUtil jwtUtil; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(jwtUtil, "SECRET_KEY", SECRET); + } + + @Test + void validateToken_returnsTheClaimsOfAValidToken() { + Claims claims = jwtUtil.validateToken(tokenWith("7", null)); + + assertNotNull(claims); + assertEquals("nurse1", claims.getSubject()); + } + + @Test + void validateToken_rejectsADenylistedToken() { + when(tokenDenylist.isTokenDenylisted("jti-1")).thenReturn(true); + + assertNull(jwtUtil.validateToken(tokenWith("7", "jti-1"))); + } + + @Test + void validateToken_keepsATokenThatIsNotDenylisted() { + when(tokenDenylist.isTokenDenylisted("jti-1")).thenReturn(false); + + assertNotNull(jwtUtil.validateToken(tokenWith("7", "jti-1"))); + } + + @Test + void validateToken_rejectsAMalformedToken() { + assertNull(jwtUtil.validateToken("not-a-token")); + } + + @Test + void extractUsername_readsTheSubjectOfTheToken() { + assertEquals("nurse1", jwtUtil.extractUsername(tokenWith("7", null))); + } + + @Test + void getUserIdFromToken_readsTheUserIdClaim() { + assertEquals("7", jwtUtil.getUserIdFromToken(tokenWith("7", null))); + assertNull(jwtUtil.getUserIdFromToken("not-a-token")); + } + + @Test + void extractAllClaims_failsForAMalformedToken() { + assertThrows(Exception.class, () -> jwtUtil.extractAllClaims("not-a-token")); + } + + @Test + void validateToken_failsWhenNoSigningSecretIsConfigured() { + ReflectionTestUtils.setField(jwtUtil, "SECRET_KEY", ""); + + assertNull(jwtUtil.validateToken(tokenWith("7", null))); + assertThrows(IllegalStateException.class, () -> jwtUtil.extractAllClaims(tokenWith("7", null))); + } + } + + @Nested + @DisplayName("TokenDenylist") + class TokenDenylistTests { + + @Mock + private RedisTemplate redisTemplate; + @Mock + private ValueOperations valueOperations; + + @InjectMocks + private TokenDenylist tokenDenylist; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void addTokenToDenylist_storesTheTokenIdUnderItsExpiry() { + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + + tokenDenylist.addTokenToDenylist("jti-1", 1000L); + + verify(valueOperations).set(anyString(), eq(" "), eq(1000L), eq(TimeUnit.MILLISECONDS)); + } + + @Test + void addTokenToDenylist_ignoresAMissingTokenId() { + tokenDenylist.addTokenToDenylist(null, 1000L); + tokenDenylist.addTokenToDenylist(" ", 1000L); + + verify(redisTemplate, never()).opsForValue(); + } + + @Test + void addTokenToDenylist_rejectsAnExpiryThatIsNotInTheFuture() { + assertThrows(IllegalArgumentException.class, () -> tokenDenylist.addTokenToDenylist("jti-1", null)); + assertThrows(IllegalArgumentException.class, () -> tokenDenylist.addTokenToDenylist("jti-1", 0L)); + } + + @Test + void addTokenToDenylist_reportsAStoreThatIsUnreachable() { + when(redisTemplate.opsForValue()).thenThrow(new RuntimeException("redis down")); + + assertThrows(RuntimeException.class, () -> tokenDenylist.addTokenToDenylist("jti-1", 1000L)); + } + + @Test + void isTokenDenylisted_readsTheStoredTokenId() { + when(redisTemplate.hasKey(anyString())).thenReturn(true); + assertTrue(tokenDenylist.isTokenDenylisted("jti-1")); + + when(redisTemplate.hasKey(anyString())).thenReturn(false); + assertFalse(tokenDenylist.isTokenDenylisted("jti-1")); + } + + @Test + void isTokenDenylisted_treatsAMissingTokenIdAsAllowed() { + assertFalse(tokenDenylist.isTokenDenylisted(null)); + assertFalse(tokenDenylist.isTokenDenylisted(" ")); + } + + @Test + void isTokenDenylisted_treatsAnUnreachableStoreAsAllowed() { + when(redisTemplate.hasKey(anyString())).thenThrow(new RuntimeException("redis down")); + + assertFalse(tokenDenylist.isTokenDenylisted("jti-1")); + } + } + + @Nested + @DisplayName("JwtAuthenticationUtil") + class JwtAuthenticationUtilTests { + + private CookieUtil cookieUtil; + private JwtUtil jwtUtil; + private RedisTemplate redisTemplate; + private ValueOperations valueOperations; + private UserLoginRepo userLoginRepo; + private JwtAuthenticationUtil authenticationUtil; + + @SuppressWarnings("unchecked") + @BeforeEach + void setUp() { + cookieUtil = mock(CookieUtil.class); + jwtUtil = mock(JwtUtil.class); + redisTemplate = mock(RedisTemplate.class); + valueOperations = mock(ValueOperations.class); + userLoginRepo = mock(UserLoginRepo.class); + + authenticationUtil = new JwtAuthenticationUtil(cookieUtil, jwtUtil); + ReflectionTestUtils.setField(authenticationUtil, "redisTemplate", redisTemplate); + ReflectionTestUtils.setField(authenticationUtil, "userLoginRepo", userLoginRepo); + } + + private Claims claimsFor(String subject, String userId) { + Claims claims = mock(Claims.class); + when(claims.getSubject()).thenReturn(subject); + when(claims.get("userId", String.class)).thenReturn(userId); + return claims; + } + + @Test + void validateJwtToken_returnsTheUsernameOfAValidToken() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(java.util.Optional.of("token")); + Claims claims = claimsFor("nurse1", "7"); + when(jwtUtil.validateToken("token")).thenReturn(claims); + + assertEquals("nurse1", authenticationUtil.validateJwtToken(request).getBody()); + } + + @Test + void validateJwtToken_rejectsARequestWithoutAToken() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(java.util.Optional.empty()); + + assertEquals(HttpStatus.UNAUTHORIZED, authenticationUtil.validateJwtToken(request).getStatusCode()); + } + + @Test + void validateJwtToken_rejectsAnInvalidToken() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(java.util.Optional.of("token")); + when(jwtUtil.validateToken("token")).thenReturn(null); + + assertTrue(authenticationUtil.validateJwtToken(request).getBody().contains("Invalid JWT Token")); + } + + @Test + void validateJwtToken_rejectsATokenWithoutAUsername() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(cookieUtil.getCookieValue(request, "Jwttoken")).thenReturn(java.util.Optional.of("token")); + Claims claims = claimsFor(null, "7"); + when(jwtUtil.validateToken("token")).thenReturn(claims); + + assertTrue(authenticationUtil.validateJwtToken(request).getBody().contains("Username is missing")); + } + + @Test + void validateUserIdAndJwtToken_acceptsAUserThatIsAlreadyCached() throws Exception { + Users cachedUser = new Users(); + cachedUser.setUserID(7L); + Claims claims = claimsFor("nurse1", "7"); + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get("user_7")).thenReturn(cachedUser); + when(userLoginRepo.getRoleNamebyUserId(7L)) + .thenReturn(new java.util.ArrayList<>(java.util.List.of("Nurse"))); + + assertTrue(authenticationUtil.validateUserIdAndJwtToken("token")); + } + + @Test + void validateUserIdAndJwtToken_cachesAUserThatWasReadFromTheDatabase() throws Exception { + Users storedUser = new Users(); + storedUser.setUserID(7L); + storedUser.setUserName("nurse1"); + Claims claims = claimsFor("nurse1", "7"); + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get("user_7")).thenReturn(null); + when(userLoginRepo.getUserByUserID(7L)).thenReturn(storedUser); + + assertTrue(authenticationUtil.validateUserIdAndJwtToken("token")); + verify(valueOperations).set(eq("user_7"), any(), eq(30L), eq(TimeUnit.MINUTES)); + } + + @Test + void validateUserIdAndJwtToken_rejectsAnInvalidToken() { + when(jwtUtil.validateToken("token")).thenReturn(null); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> authenticationUtil.validateUserIdAndJwtToken("token")); + assertTrue(thrown.getMessage().contains("Invalid JWT token")); + } + + @Test + void validateUserIdAndJwtToken_rejectsAnUnknownUser() { + Claims claims = claimsFor("nurse1", "7"); + when(jwtUtil.validateToken("token")).thenReturn(claims); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get("user_7")).thenReturn(null); + when(userLoginRepo.getUserByUserID(7L)).thenReturn(null); + + IEMRException thrown = assertThrows(IEMRException.class, + () -> authenticationUtil.validateUserIdAndJwtToken("token")); + assertTrue(thrown.getMessage().contains("Invalid User ID")); + } + + @Test + void getUserRoles_returnsTheRolesOfTheUser() throws Exception { + when(userLoginRepo.getRoleNamebyUserId(7L)) + .thenReturn(new java.util.ArrayList<>(java.util.List.of("Nurse"))); + + assertEquals(java.util.List.of("Nurse"), authenticationUtil.getUserRoles(7L)); + } + + @Test + void getUserRoles_rejectsAnInvalidUserId() { + assertThrows(IEMRException.class, () -> authenticationUtil.getUserRoles(null)); + assertThrows(IEMRException.class, () -> authenticationUtil.getUserRoles(0L)); + } + + @Test + void getUserRoles_failsWhenTheUserHasNoRole() { + when(userLoginRepo.getRoleNamebyUserId(7L)).thenReturn(new java.util.ArrayList<>()); + + assertThrows(IEMRException.class, () -> authenticationUtil.getUserRoles(7L)); + } + } + + @Nested + @DisplayName("UserAgentContext") + class UserAgentContextTests { + + @Test + void theUserAgentIsHeldPerThreadUntilItIsCleared() { + UserAgentContext.setUserAgent("okhttp/4.9"); + assertEquals("okhttp/4.9", UserAgentContext.getUserAgent()); + + UserAgentContext.clear(); + assertNull(UserAgentContext.getUserAgent()); + } + } + + @Nested + @DisplayName("MediaTypeUtils") + class MediaTypeUtilsTests { + + @Test + void getMediaTypeForFileName_readsTheTypeFromTheServletContext() { + jakarta.servlet.ServletContext servletContext = mock(jakarta.servlet.ServletContext.class); + when(servletContext.getMimeType("report.pdf")).thenReturn("application/pdf"); + + assertEquals("application/pdf", + MediaTypeUtils.getMediaTypeForFileName(servletContext, "report.pdf").toString()); + } + + @Test + void getMediaTypeForFileName_fallsBackToOctetStreamForAnUnknownType() { + jakarta.servlet.ServletContext servletContext = mock(jakarta.servlet.ServletContext.class); + when(servletContext.getMimeType("report.xyz")).thenReturn(null); + + assertEquals("application/octet-stream", + MediaTypeUtils.getMediaTypeForFileName(servletContext, "report.xyz").toString()); + } + } + + @Nested + @DisplayName("RestTemplateUtil") + class RestTemplateUtilTests { + + @Test + void createRequestEntity_sendsTheAuthorizationAsABearerTokenAndTheJwtAsACookie() { + org.springframework.http.HttpEntity entity = RestTemplateUtil.createRequestEntity("{}", "auth", + "jwt"); + + assertEquals("Bearer auth", entity.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + assertEquals("Jwttoken=jwt", entity.getHeaders().getFirst(HttpHeaders.COOKIE)); + assertTrue(entity.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE).contains("charset=utf-8")); + } + + @Test + void createRequestEntity_sendsTheRawAuthorizationForADataSyncCall() { + org.springframework.http.HttpEntity entity = RestTemplateUtil.createRequestEntity("{}", "auth", + "datasync"); + + assertEquals("auth", entity.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + assertNull(entity.getHeaders().getFirst(HttpHeaders.COOKIE)); + } + + @Test + void createRequestEntity_readsTheJwtFromTheCurrentRequestWhenNoneWasPassed() { + org.springframework.mock.web.MockHttpServletRequest request = + new org.springframework.mock.web.MockHttpServletRequest(); + request.setCookies(new jakarta.servlet.http.Cookie("Jwttoken", "from-cookie")); + org.springframework.web.context.request.RequestContextHolder.setRequestAttributes( + new org.springframework.web.context.request.ServletRequestAttributes(request)); + try { + org.springframework.http.HttpEntity entity = RestTemplateUtil.createRequestEntity("{}", + "auth", ""); + + assertEquals("Jwttoken=from-cookie", entity.getHeaders().getFirst(HttpHeaders.COOKIE)); + } finally { + org.springframework.web.context.request.RequestContextHolder.resetRequestAttributes(); + } + } + + @Test + void createRequestEntity_omitsTheCookieWhenThereIsNoCurrentRequest() { + org.springframework.web.context.request.RequestContextHolder.resetRequestAttributes(); + + org.springframework.http.HttpEntity entity = RestTemplateUtil.createRequestEntity("{}", "", ""); + + assertNull(entity.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + assertNull(entity.getHeaders().getFirst(HttpHeaders.COOKIE)); + } + } +} diff --git a/src/test/java/com/iemr/mmu/utils/JwtUserIdValidationFilterTest.java b/src/test/java/com/iemr/mmu/utils/JwtUserIdValidationFilterTest.java new file mode 100644 index 00000000..ba91727a --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/JwtUserIdValidationFilterTest.java @@ -0,0 +1,242 @@ +/* +* 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.mmu.utils; + +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.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.iemr.mmu.utils.exception.IEMRException; +import com.iemr.mmu.utils.http.AuthorizationHeaderRequestWrapper; +import com.iemr.mmu.utils.mapper.RoleAuthenticationFilter; +import com.iemr.mmu.utils.redis.RedisStorage; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.Cookie; + +class JwtUserIdValidationFilterTest { + + private JwtAuthenticationUtil jwtAuthenticationUtil; + private JwtUserIdValidationFilter filter; + private FilterChain chain; + + @BeforeEach + void setUp() { + jwtAuthenticationUtil = mock(JwtAuthenticationUtil.class); + filter = new JwtUserIdValidationFilter(jwtAuthenticationUtil, "https://mmu.example.org,http://localhost:*"); + chain = mock(FilterChain.class); + } + + @AfterEach + void clearSecurityContext() { + SecurityContextHolder.clearContext(); + UserAgentContext.clear(); + } + + private MockHttpServletRequest request(String method, String uri) { + MockHttpServletRequest request = new MockHttpServletRequest(method, uri); + request.setRequestURI(uri); + return request; + } + + @Test + @DisplayName("a preflight from an allowed origin is answered with the CORS headers") + void doFilter_answersAPreflightFromAnAllowedOrigin() throws Exception { + MockHttpServletRequest request = request("OPTIONS", "/ANC/save"); + request.addHeader("Origin", "https://mmu.example.org"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request, response, chain); + + assertEquals(200, response.getStatus()); + assertEquals("https://mmu.example.org", response.getHeader("Access-Control-Allow-Origin")); + assertEquals("true", response.getHeader("Access-Control-Allow-Credentials")); + verify(chain, never()).doFilter(any(), any()); + } + + @Test + @DisplayName("a wildcard port on localhost is an allowed origin") + void doFilter_allowsAWildcardLocalhostPort() throws Exception { + MockHttpServletRequest request = request("OPTIONS", "/ANC/save"); + request.addHeader("Origin", "http://localhost:4200"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request, response, chain); + + assertEquals(200, response.getStatus()); + } + + @Test + @DisplayName("a preflight without an Origin header is refused") + void doFilter_refusesAPreflightWithoutAnOrigin() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request("OPTIONS", "/ANC/save"), response, chain); + + assertEquals(403, response.getStatus()); + } + + @Test + @DisplayName("a preflight from an unknown origin is refused") + void doFilter_refusesAPreflightFromAnUnknownOrigin() throws Exception { + MockHttpServletRequest request = request("OPTIONS", "/ANC/save"); + request.addHeader("Origin", "https://attacker.example.org"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request, response, chain); + + assertEquals(403, response.getStatus()); + } + + @Test + @DisplayName("a request from an unknown origin is refused") + void doFilter_refusesARequestFromAnUnknownOrigin() throws Exception { + MockHttpServletRequest request = request("POST", "/ANC/save"); + request.addHeader("Origin", "https://attacker.example.org"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request, response, chain); + + assertEquals(403, response.getStatus()); + } + + @Test + @DisplayName("public endpoints are let through without a token") + void doFilter_letsPublicEndpointsThrough() throws Exception { + for (String path : List.of("/user/userAuthenticate", "/user/logOutUserFromConcurrentSession", + "/swagger-ui/index.html", "/v3/api-docs", "/user/refreshToken", "/public/thing", "/version", + "/health")) { + FilterChain freshChain = mock(FilterChain.class); + + filter.doFilter(request("POST", path), new MockHttpServletResponse(), freshChain); + + verify(freshChain).doFilter(any(), any()); + } + } + + @Test + @DisplayName("a valid JWT cookie lets the request through and clears any userId cookie") + void doFilter_letsAValidJwtCookieThrough() throws Exception { + MockHttpServletRequest request = request("POST", "/ANC/save"); + request.setCookies(new Cookie("Jwttoken", "token"), new Cookie("userId", "7")); + MockHttpServletResponse response = new MockHttpServletResponse(); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken("token")).thenReturn(true); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(any(AuthorizationHeaderRequestWrapper.class), any()); + assertEquals(0, response.getCookie("userId").getMaxAge()); + } + + @Test + @DisplayName("a valid JWT header lets the request through") + void doFilter_letsAValidJwtHeaderThrough() throws Exception { + MockHttpServletRequest request = request("POST", "/ANC/save"); + request.addHeader("JwtToken", "token"); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken("token")).thenReturn(true); + + filter.doFilter(request, new MockHttpServletResponse(), chain); + + verify(chain).doFilter(any(AuthorizationHeaderRequestWrapper.class), any()); + } + + @Test + @DisplayName("a mobile client with an Authorization header is let through") + void doFilter_letsAMobileClientThrough() throws Exception { + MockHttpServletRequest request = request("POST", "/ANC/save"); + request.addHeader("User-Agent", "okhttp/4.9.0"); + request.addHeader("Authorization", "session-key"); + + filter.doFilter(request, new MockHttpServletResponse(), chain); + + verify(chain).doFilter(any(), any()); + } + + @Test + @DisplayName("a request with no recognisable token is refused") + void doFilter_refusesARequestWithNoToken() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request("POST", "/ANC/save"), response, chain); + + assertEquals(401, response.getStatus()); + } + + @Test + @DisplayName("a token that fails validation is refused") + void doFilter_refusesATokenThatFailsValidation() throws Exception { + MockHttpServletRequest request = request("POST", "/ANC/save"); + request.setCookies(new Cookie("Jwttoken", "token")); + MockHttpServletResponse response = new MockHttpServletResponse(); + when(jwtAuthenticationUtil.validateUserIdAndJwtToken("token")) + .thenThrow(new IEMRException("Invalid User ID.")); + + filter.doFilter(request, response, chain); + + assertEquals(401, response.getStatus()); + } + + @Test + @DisplayName("no configured origins means no origin is allowed") + void doFilter_refusesEveryOriginWhenNoneIsConfigured() throws Exception { + JwtUserIdValidationFilter noOriginsFilter = new JwtUserIdValidationFilter(jwtAuthenticationUtil, " "); + MockHttpServletRequest request = request("OPTIONS", "/ANC/save"); + request.addHeader("Origin", "https://mmu.example.org"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + noOriginsFilter.doFilter(request, response, chain); + + assertEquals(403, response.getStatus()); + } + + @Test + @DisplayName("the Authorization header wrapper reports the header it was given") + void authorizationHeaderRequestWrapper_reportsTheHeaderItWasGiven() { + MockHttpServletRequest request = request("POST", "/ANC/save"); + request.addHeader("Accept", "application/json"); + + AuthorizationHeaderRequestWrapper wrapper = new AuthorizationHeaderRequestWrapper(request, "session-key"); + + assertEquals("session-key", wrapper.getHeader("authorization")); + assertEquals("session-key", wrapper.getHeaders("Authorization").nextElement()); + assertEquals("application/json", wrapper.getHeader("Accept")); + assertTrue(java.util.Collections.list(wrapper.getHeaderNames()).contains("Authorization")); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/config/ConfigPropertiesTest.java b/src/test/java/com/iemr/mmu/utils/config/ConfigPropertiesTest.java new file mode 100644 index 00000000..92433b1e --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/config/ConfigPropertiesTest.java @@ -0,0 +1,75 @@ +/* +* 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.mmu.utils.config; + +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 org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ConfigPropertiesTest { + + @Test + @DisplayName("a property that is not in the file reads back as null") + void getPropertyByName_returnsNothingForAnUnknownProperty() { + assertNull(ConfigProperties.getPropertyByName("no.such.property")); + } + + @Test + @DisplayName("an unparseable numeric property falls back to its zero value") + void theNumericReadersFallBackToZeroForAnUnparseableProperty() { + assertEquals(0, ConfigProperties.getInteger("no.such.property")); + assertEquals(0L, ConfigProperties.getLong("no.such.property")); + } + + @Test + @DisplayName("a numeric property that is present is read as a number") + void theNumericReadersReadAStoredNumber() { + assertEquals(1800, ConfigProperties.getInteger("iemr.session.expiry.time")); + assertEquals(1800L, ConfigProperties.getLong("iemr.session.expiry.time")); + assertEquals(1800F, ConfigProperties.getFloat("iemr.session.expiry.time")); + } + + @Test + @DisplayName("a missing boolean property reads back as false") + void getBoolean_returnsFalseForAMissingProperty() { + assertFalse(ConfigProperties.getBoolean("no.such.property")); + } + + @Test + @DisplayName("the Redis connection settings are readable") + void theRedisConnectionSettingsAreReadable() { + ConfigProperties.getRedisUrl(); + + assertEquals(0, ConfigProperties.getRedisPort()); + } + + @Test + @DisplayName("the session expiry settings are readable") + void theSessionExpirySettingsAreReadable() { + assertFalse(ConfigProperties.getExtendExpiryTime()); + org.junit.jupiter.api.Assertions.assertTrue(ConfigProperties.getSessionExpiryTime() > 0); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/exception/CustomExceptionResponseTest.java b/src/test/java/com/iemr/mmu/utils/exception/CustomExceptionResponseTest.java new file mode 100644 index 00000000..01e7937a --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/exception/CustomExceptionResponseTest.java @@ -0,0 +1,137 @@ +/* +* 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.mmu.utils.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.ConnectException; +import java.sql.SQLException; +import java.text.ParseException; + +import org.json.JSONException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CustomExceptionResponseTest { + + @Test + @DisplayName("a JSON object response is passed through as data") + void setResponse_keepsAJsonObjectAsData() { + CustomExceptionResponse response = new CustomExceptionResponse(); + response.setResponse("{\"name\":\"Asha\"}"); + + assertTrue(response.isSuccess()); + assertEquals(CustomExceptionResponse.SUCCESS, response.getStatusCode()); + assertEquals("{\"name\":\"Asha\"}", response.getData()); + } + + @Test + @DisplayName("a JSON array response is passed through as data") + void setResponse_keepsAJsonArrayAsData() { + CustomExceptionResponse response = new CustomExceptionResponse(); + response.setResponse("[{\"name\":\"Asha\"}]"); + + assertTrue(response.getData().startsWith("[")); + } + + @Test + @DisplayName("a plain message response is wrapped in a response object") + void setResponse_wrapsAPlainMessage() { + CustomExceptionResponse response = new CustomExceptionResponse(); + response.setResponse("Data saved successfully"); + + assertTrue(response.getData().contains("Data saved successfully")); + } + + @Test + @DisplayName("each database failure maps to the database status code") + void setError_mapsEveryDatabaseFailureToTheDatabaseCode() { + assertEquals(CustomExceptionResponse.DB_EXCEPTION, errorFor(new SQLException("bad sql"))); + assertEquals(CustomExceptionResponse.DB_EXCEPTION, + errorFor(new org.hibernate.exception.SQLGrammarException("bad grammar", new SQLException()))); + assertEquals(CustomExceptionResponse.DB_EXCEPTION, + errorFor(new org.hibernate.exception.DataException("bad data", new SQLException()))); + assertEquals(CustomExceptionResponse.DB_EXCEPTION, + errorFor(new org.hibernate.exception.ConstraintViolationException("violated", new SQLException(), + "constraint"))); + assertEquals(CustomExceptionResponse.DB_EXCEPTION, + errorFor(new org.hibernate.exception.GenericJDBCException("generic", new SQLException()))); + assertEquals(CustomExceptionResponse.DB_EXCEPTION, + errorFor(new org.hibernate.exception.JDBCConnectionException("no connection", new SQLException()))); + assertEquals(CustomExceptionResponse.DB_EXCEPTION, + errorFor(new org.hibernate.exception.LockAcquisitionException("locked", new SQLException()))); + assertEquals(CustomExceptionResponse.DB_EXCEPTION, errorFor( + new org.springframework.dao.InvalidDataAccessResourceUsageException("bad usage"))); + } + + @Test + @DisplayName("each remaining failure maps to its own status code") + void setError_mapsEachRemainingFailureToItsStatusCode() { + assertEquals(CustomExceptionResponse.USERID_FAILURE, errorFor(new IEMRException("bad user"))); + assertEquals(CustomExceptionResponse.OBJECT_FAILURE, errorFor(new JSONException("bad json"))); + assertEquals(CustomExceptionResponse.ENVIRONMENT_EXCEPTION, errorFor(new ParseException("bad date", 0))); + assertEquals(CustomExceptionResponse.ENVIRONMENT_EXCEPTION, errorFor(new NullPointerException("npe"))); + assertEquals(CustomExceptionResponse.ENVIRONMENT_EXCEPTION, + errorFor(new ArrayIndexOutOfBoundsException("index"))); + assertEquals(CustomExceptionResponse.ENVIRONMENT_EXCEPTION, errorFor(new IOException("io"))); + assertEquals(CustomExceptionResponse.ENVIRONMENT_EXCEPTION, errorFor(new ConnectException("refused"))); + assertEquals(CustomExceptionResponse.GENERIC_FAILURE, errorFor(new IllegalStateException("boom"))); + } + + /** The response reads the cause, so every failure is wrapped before it is reported. */ + private int errorFor(Throwable cause) { + CustomExceptionResponse response = new CustomExceptionResponse(); + response.setError(new Exception("wrapped", cause)); + return response.getStatusCode(); + } + + @Test + @DisplayName("an explicit error carries its own code, message and status") + void setError_carriesTheGivenCodeAndMessage() { + CustomExceptionResponse response = new CustomExceptionResponse(); + response.setError(404, "Not found", "NOT_FOUND"); + + assertEquals(404, response.getStatusCode()); + assertEquals("Not found", response.getErrorMessage()); + assertEquals("NOT_FOUND", response.getStatus()); + assertFalse(response.isSuccess()); + + response.setError(400, "Bad request"); + assertEquals("Bad request", response.getStatus()); + } + + @Test + @DisplayName("a failed response carries no data") + void getData_returnsNothingForAFailedResponse() { + assertNull(new CustomExceptionResponse().getData()); + } + + @Test + @DisplayName("serialising with nulls keeps the empty data field") + void toStringWithSerialization_keepsTheEmptyDataField() { + assertTrue(new CustomExceptionResponse().toStringWithSerialization().contains("\"data\":null")); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/exception/SecurityExceptionHandlersTest.java b/src/test/java/com/iemr/mmu/utils/exception/SecurityExceptionHandlersTest.java new file mode 100644 index 00000000..75fbe020 --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/exception/SecurityExceptionHandlersTest.java @@ -0,0 +1,80 @@ +/* +* 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.mmu.utils.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; + +class SecurityExceptionHandlersTest { + + @Test + @DisplayName("an IEMR exception carries its message and error code") + void iemrException_carriesItsMessageAndErrorCode() { + IEMRException withCause = new IEMRException("wrapped", new RuntimeException("cause")); + assertEquals("wrapped", withCause.getMessage()); + assertEquals("wrapped", withCause.toString()); + assertNull(withCause.getErrorCode()); + + IEMRException withCode = new IEMRException("coded", 5002); + assertEquals(5002, withCode.getErrorCode()); + + withCode.setErrorCode(400); + assertEquals(400, withCode.getErrorCode()); + } + + @Test + @DisplayName("a denied request is answered with a 403 and a JSON body") + void customAccessDeniedHandler_answersWithForbidden() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + + new CustomAccessDeniedHandler().handle(new MockHttpServletRequest(), response, + new AccessDeniedException("denied")); + + assertEquals(403, response.getStatus()); + assertEquals("application/json", response.getContentType()); + assertTrue(response.getContentAsString().contains("Access denied")); + } + + @Test + @DisplayName("an unauthenticated request is answered with a 401 and a JSON body") + void customAuthenticationEntryPoint_answersWithUnauthorized() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + + new CustomAuthenticationEntryPoint().commence(new MockHttpServletRequest(), response, + new BadCredentialsException("no token")); + + assertEquals(401, response.getStatus()); + assertTrue(response.getContentAsString().contains("Unauthorized")); + assertTrue(response.getContentAsString().contains("no token")); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/http/HttpUtilsAndInterceptorTest.java b/src/test/java/com/iemr/mmu/utils/http/HttpUtilsAndInterceptorTest.java new file mode 100644 index 00000000..83eb0d18 --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/http/HttpUtilsAndInterceptorTest.java @@ -0,0 +1,261 @@ +/* +* 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.mmu.utils.http; + +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.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.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +import com.iemr.mmu.utils.exception.IEMRException; +import com.iemr.mmu.utils.sessionobject.SessionObject; +import com.iemr.mmu.utils.validator.Validator; + +class HttpUtilsAndInterceptorTest { + + @Nested + @DisplayName("HttpUtils") + class HttpUtilsTests { + + private HttpUtils httpUtils; + private RestTemplate restTemplate; + + @BeforeEach + void setUp() { + httpUtils = new HttpUtils(); + restTemplate = mock(RestTemplate.class); + ReflectionTestUtils.setField(httpUtils, "rest", restTemplate); + } + + private void stubExchange(HttpMethod method, String body) { + when(restTemplate.exchange(anyString(), eq(method), any(), eq(String.class))) + .thenReturn(new ResponseEntity<>(body, HttpStatus.OK)); + } + + @Test + void get_returnsTheResponseBodyAndRecordsTheStatus() { + stubExchange(HttpMethod.GET, "body"); + + assertEquals("body", httpUtils.get("http://service/thing")); + assertEquals(HttpStatus.OK, httpUtils.getStatus()); + } + + @Test + void get_sendsTheAuthorizationAndContentTypeItWasGiven() { + stubExchange(HttpMethod.GET, "body"); + HashMap header = new HashMap<>(); + header.put("Authorization", "session-key"); + header.put("Content-Type", "application/json"); + + assertEquals("body", httpUtils.get("http://service/thing", header)); + } + + @Test + void get_fallsBackToJsonWhenNoContentTypeWasGiven() { + stubExchange(HttpMethod.GET, "body"); + + assertEquals("body", httpUtils.get("http://service/thing", new HashMap<>())); + } + + @Test + void post_returnsTheResponseBody() { + stubExchange(HttpMethod.POST, "body"); + + assertEquals("body", httpUtils.post("http://service/thing", "{}")); + } + + @Test + void post_sendsTheAuthorizationItWasGiven() { + stubExchange(HttpMethod.POST, "body"); + HashMap header = new HashMap<>(); + header.put("Authorization", "session-key"); + + assertEquals("body", httpUtils.post("http://service/thing", "{}", header)); + } + + @Test + void setStatus_isReadBackByGetStatus() { + httpUtils.setStatus(HttpStatus.NOT_FOUND); + + assertEquals(HttpStatus.NOT_FOUND, httpUtils.getStatus()); + } + } + + @Nested + @DisplayName("HttpInterceptor") + class HttpInterceptorTests { + + private HttpInterceptor interceptor; + private Validator validator; + private SessionObject sessionObject; + + @BeforeEach + void setUp() { + interceptor = new HttpInterceptor(); + validator = mock(Validator.class); + sessionObject = mock(SessionObject.class); + interceptor.setValidator(validator); + interceptor.setSessionObject(sessionObject); + ReflectionTestUtils.setField(interceptor, "allowedOrigins", "https://mmu.example.org"); + } + + private MockHttpServletRequest request(String method, String uri, String authorization) { + MockHttpServletRequest request = new MockHttpServletRequest(method, uri); + request.setRequestURI(uri); + if (authorization != null) { + request.addHeader("Authorization", authorization); + } + return request; + } + + @Test + void preHandle_letsAnUnauthenticatedRequestThrough() throws Exception { + assertTrue(interceptor.preHandle(request("POST", "/ANC/save", null), new MockHttpServletResponse(), + null)); + verify(validator, never()).checkKeyExists(anyString(), anyString()); + } + + @Test + void preHandle_skipsValidationForThePublicEndpoints() throws Exception { + for (String endpoint : java.util.List.of("userAuthenticate", "superUserAuthenticate", "userLogout", + "changePassword", "swagger-ui.html", "api-docs", "startMasterDownload")) { + assertTrue(interceptor.preHandle(request("POST", "/user/" + endpoint, "session-key"), + new MockHttpServletResponse(), null)); + } + verify(validator, never()).checkKeyExists(anyString(), anyString()); + } + + @Test + void preHandle_stopsTheErrorEndpoint() throws Exception { + assertFalse(interceptor.preHandle(request("POST", "/error", "session-key"), + new MockHttpServletResponse(), null)); + } + + @Test + void preHandle_validatesTheSessionOfEveryOtherRequest() throws Exception { + MockHttpServletRequest request = request("POST", "/ANC/save", "session-key"); + request.addHeader("X-FORWARDED-FOR", "10.0.0.1"); + + assertTrue(interceptor.preHandle(request, new MockHttpServletResponse(), null)); + verify(validator).checkKeyExists("session-key", "10.0.0.1"); + } + + @Test + void preHandle_fallsBackToTheRemoteAddressWhenThereIsNoForwardedHeader() throws Exception { + MockHttpServletRequest request = request("POST", "/ANC/save", "session-key"); + request.setRemoteAddr("10.0.0.2"); + + assertTrue(interceptor.preHandle(request, new MockHttpServletResponse(), null)); + verify(validator).checkKeyExists("session-key", "10.0.0.2"); + } + + @Test + void preHandle_stopsAndAnswersWhenTheSessionIsExpired() throws Exception { + MockHttpServletRequest request = request("POST", "/ANC/save", "session-key"); + request.addHeader("Origin", "https://mmu.example.org"); + MockHttpServletResponse response = new MockHttpServletResponse(); + doThrow(new IEMRException("Session is expired. Please login again.")).when(validator) + .checkKeyExists(anyString(), anyString()); + + assertFalse(interceptor.preHandle(request, response, null)); + assertEquals("https://mmu.example.org", response.getHeader("Access-Control-Allow-Origin")); + assertTrue(response.getContentAsString().contains("Session is expired")); + } + + @Test + void preHandle_omitsTheCorsHeadersForAnUnknownOrigin() throws Exception { + MockHttpServletRequest request = request("POST", "/ANC/save", "session-key"); + request.addHeader("Origin", "https://attacker.example.org"); + MockHttpServletResponse response = new MockHttpServletResponse(); + doThrow(new IEMRException("Session is expired.")).when(validator).checkKeyExists(anyString(), + anyString()); + + assertFalse(interceptor.preHandle(request, response, null)); + assertNull(response.getHeader("Access-Control-Allow-Origin")); + } + + @Test + void preHandle_skipsValidationForAPreflight() throws Exception { + assertTrue(interceptor.preHandle(request("OPTIONS", "/ANC/save", "session-key"), + new MockHttpServletResponse(), null)); + verify(validator, never()).checkKeyExists(anyString(), anyString()); + } + + @Test + void postHandle_refreshesTheSessionOfAnAuthenticatedRequest() throws Exception { + when(sessionObject.getSessionObject("session-key")).thenReturn("{}"); + + interceptor.postHandle(request("POST", "/ANC/save", "session-key"), new MockHttpServletResponse(), null, + null); + + verify(sessionObject).updateSessionObject("session-key", "{}"); + } + + @Test + void postHandle_leavesAnUnauthenticatedRequestAlone() throws Exception { + interceptor.postHandle(request("POST", "/ANC/save", null), new MockHttpServletResponse(), null, null); + + verify(sessionObject, never()).updateSessionObject(anyString(), anyString()); + } + + @Test + void postHandle_swallowsAFailedSessionRefresh() throws Exception { + when(sessionObject.getSessionObject("session-key")) + .thenThrow(new com.iemr.mmu.utils.redis.RedisSessionException("redis down")); + + interceptor.postHandle(request("POST", "/ANC/save", "session-key"), new MockHttpServletResponse(), null, + null); + } + + @Test + void afterCompletion_doesNothingToTheResponse() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + + interceptor.afterCompletion(request("POST", "/ANC/save", null), response, null, null); + + assertEquals(200, response.getStatus()); + } + } +} diff --git a/src/test/java/com/iemr/mmu/utils/mapper/MapperTest.java b/src/test/java/com/iemr/mmu/utils/mapper/MapperTest.java new file mode 100644 index 00000000..c8187aa1 --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/mapper/MapperTest.java @@ -0,0 +1,72 @@ +/* +* 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.mmu.utils.mapper; + +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 org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.google.gson.JsonParser; +import com.iemr.mmu.data.registrar.BeneficiaryData; + +class MapperTest { + + @Test + @DisplayName("the input mapper reads the ISO date format used by the UI") + void inputMapper_readsTheIsoDateFormat() throws Exception { + BeneficiaryData beneficiary = InputMapper.gson() + .fromJson("{\"firstName\":\"Asha\",\"dob\":\"1990-05-04T00:00:00.000\"}", BeneficiaryData.class); + + assertEquals("Asha", beneficiary.getFirstName()); + assertNotNull(beneficiary.getDob()); + } + + @Test + @DisplayName("the input mapper reads a JSON element as well as a string") + void inputMapper_readsAJsonElement() throws Exception { + BeneficiaryData beneficiary = InputMapper.gson() + .fromJson(JsonParser.parseString("{\"firstName\":\"Asha\"}"), BeneficiaryData.class); + + assertEquals("Asha", beneficiary.getFirstName()); + } + + @Test + @DisplayName("the flagged input mapper reads the long-form date format") + void inputMapper_readsTheLongFormDateFormat() throws Exception { + BeneficiaryData beneficiary = InputMapper.gson(1) + .fromJson("{\"dob\":\"May 04, 1990 00:00:00\"}", BeneficiaryData.class, 1); + + assertNotNull(beneficiary.getDob()); + } + + @Test + @DisplayName("the output mapper serialises nulls and exposed fields only") + void outputMapper_serialisesNullsAndExposedFieldsOnly() { + new OutputMapper(); + + assertNotNull(OutputMapper.gson()); + assertEquals("{}", OutputMapper.gson().toJson(new Object())); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/mapper/RoleAuthenticationFilterTest.java b/src/test/java/com/iemr/mmu/utils/mapper/RoleAuthenticationFilterTest.java new file mode 100644 index 00000000..3e7c705c --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/mapper/RoleAuthenticationFilterTest.java @@ -0,0 +1,200 @@ +/* +* 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.mmu.utils.mapper; + +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.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.iemr.mmu.data.login.Users; +import com.iemr.mmu.utils.JwtAuthenticationUtil; +import com.iemr.mmu.utils.JwtUtil; +import com.iemr.mmu.utils.exception.IEMRException; +import com.iemr.mmu.utils.redis.RedisStorage; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.Cookie; + +class RoleAuthenticationFilterTest { + + @Mock + private JwtUtil jwtUtil; + @Mock + private RedisStorage redisService; + @Mock + private JwtAuthenticationUtil userService; + + @InjectMocks + private RoleAuthenticationFilter filter; + + private FilterChain chain; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + chain = mock(FilterChain.class); + } + + @AfterEach + void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + private MockHttpServletRequest requestWithJwtCookie(String token) { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/ANC/save"); + request.setCookies(new Cookie("Jwttoken", token)); + return request; + } + + private Claims claimsWithUserId(Object userId) { + Claims claims = mock(Claims.class); + when(claims.get("userId")).thenReturn(userId); + return claims; + } + + @Test + @DisplayName("cached roles are turned into authorities without hitting the user service") + void doFilterInternal_usesTheCachedRoles() throws Exception { + Claims claims = claimsWithUserId("7"); + when(jwtUtil.extractAllClaims("token")).thenReturn(claims); + when(redisService.getUserRoleFromCache(7L)).thenReturn(new ArrayList<>(List.of("ROLE_NURSE"))); + + filter.doFilter(requestWithJwtCookie("token"), new MockHttpServletResponse(), chain); + + assertEquals("7", SecurityContextHolder.getContext().getAuthentication().getPrincipal()); + assertEquals("ROLE_NURSE", + SecurityContextHolder.getContext().getAuthentication().getAuthorities().iterator().next() + .getAuthority()); + verify(userService, never()).getUserRoles(anyLong()); + } + + @Test + @DisplayName("roles read from the user service are normalised and cached") + void doFilterInternal_normalisesAndCachesFreshlyReadRoles() throws Exception { + Claims claims = claimsWithUserId("7"); + when(jwtUtil.extractAllClaims("token")).thenReturn(claims); + when(redisService.getUserRoleFromCache(7L)).thenReturn(new ArrayList<>()); + when(userService.getUserRoles(7L)) + .thenReturn(new ArrayList<>(Arrays.asList(" lab technician ", null))); + + filter.doFilter(requestWithJwtCookie("token"), new MockHttpServletResponse(), chain); + + assertEquals("ROLE_LAB_TECHNICIAN", + SecurityContextHolder.getContext().getAuthentication().getAuthorities().iterator().next() + .getAuthority()); + verify(redisService).cacheUserRoles(7L, List.of("ROLE_LAB_TECHNICIAN")); + } + + @Test + @DisplayName("the JWT header is used when there is no cookie") + void doFilterInternal_fallsBackToTheJwtHeader() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/ANC/save"); + request.addHeader("Jwttoken", "token"); + Claims claims = claimsWithUserId("7"); + when(jwtUtil.extractAllClaims("token")).thenReturn(claims); + when(redisService.getUserRoleFromCache(7L)).thenReturn(new ArrayList<>(List.of("ROLE_NURSE"))); + + filter.doFilter(request, new MockHttpServletResponse(), chain); + + assertNotNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("a request without a token is passed straight through") + void doFilterInternal_passesARequestWithoutATokenStraightThrough() throws Exception { + filter.doFilter(new MockHttpServletRequest("POST", "/ANC/save"), new MockHttpServletResponse(), chain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + // The early return and the finally block both forward the request. + verify(chain, org.mockito.Mockito.times(2)).doFilter(any(), any()); + } + + @Test + @DisplayName("a token with no readable claims is passed straight through") + void doFilterInternal_passesATokenWithNoClaimsStraightThrough() throws Exception { + when(jwtUtil.extractAllClaims("token")).thenReturn(null); + + filter.doFilter(requestWithJwtCookie("token"), new MockHttpServletResponse(), chain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("a token without a user id is passed straight through") + void doFilterInternal_passesATokenWithoutAUserIdStraightThrough() throws Exception { + Claims claims = claimsWithUserId(null); + when(jwtUtil.extractAllClaims("token")).thenReturn(claims); + + filter.doFilter(requestWithJwtCookie("token"), new MockHttpServletResponse(), chain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("a token with a non-numeric user id is passed straight through") + void doFilterInternal_passesANonNumericUserIdStraightThrough() throws Exception { + Claims claims = claimsWithUserId("not-a-number"); + when(jwtUtil.extractAllClaims("token")).thenReturn(claims); + + filter.doFilter(requestWithJwtCookie("token"), new MockHttpServletResponse(), chain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("a failure while reading the roles clears the security context") + void doFilterInternal_clearsTheSecurityContextWhenTheRolesCannotBeRead() throws Exception { + Claims claims = claimsWithUserId("7"); + when(jwtUtil.extractAllClaims("token")).thenReturn(claims); + when(redisService.getUserRoleFromCache(7L)).thenReturn(new ArrayList<>()); + when(userService.getUserRoles(7L)).thenThrow(new IEMRException("No role found for userId : 7")); + + filter.doFilter(requestWithJwtCookie("token"), new MockHttpServletResponse(), chain); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + verify(chain).doFilter(any(), any()); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/redis/RedisStorageTest.java b/src/test/java/com/iemr/mmu/utils/redis/RedisStorageTest.java new file mode 100644 index 00000000..58efcf61 --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/redis/RedisStorageTest.java @@ -0,0 +1,170 @@ +/* +* 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.mmu.utils.redis; + +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.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; + +class RedisStorageTest { + + @Mock + private LettuceConnectionFactory connectionFactory; + @Mock + private RedisConnection redisConnection; + @Mock + private RedisTemplate redisTemplate; + @Mock + private ListOperations listOperations; + + @InjectMocks + private RedisStorage redisStorage; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(connectionFactory.getConnection()).thenReturn(redisConnection); + } + + @Test + @DisplayName("a session is stored only when the key is still free") + void setObject_storesTheSessionWhenTheKeyIsFree() { + when(redisConnection.get("key".getBytes())).thenReturn(null); + + assertEquals("key", redisStorage.setObject("key", "value", 60)); + verify(redisConnection).set(any(), any(), any(), any()); + } + + @Test + @DisplayName("an existing session is left untouched") + void setObject_leavesAnExistingSessionUntouched() { + when(redisConnection.get("key".getBytes())).thenReturn("existing".getBytes()); + + assertEquals("key", redisStorage.setObject("key", "value", 60)); + verify(redisConnection, org.mockito.Mockito.never()).set(any(), any(), any(), any()); + } + + @Test + @DisplayName("reading a session refreshes its expiry") + void getObject_refreshesTheExpiryOfAStoredSession() throws Exception { + when(redisConnection.get("key".getBytes())).thenReturn("value".getBytes()); + + assertEquals("value", redisStorage.getObject("key", 60)); + verify(redisConnection).expire("key".getBytes(), 60); + } + + @Test + @DisplayName("reading a session that is not stored fails") + void getObject_failsWhenTheSessionIsNotStored() { + when(redisConnection.get("key".getBytes())).thenReturn(null); + + assertThrows(RedisSessionException.class, () -> redisStorage.getObject("key", 60)); + } + + @Test + @DisplayName("deleting a session reports how many keys were removed") + void deleteObject_reportsHowManyKeysWereRemoved() throws Exception { + when(redisConnection.del("key".getBytes())).thenReturn(1L); + + assertEquals(1L, redisStorage.deleteObject("key")); + } + + @Test + @DisplayName("updating a stored session rewrites its value") + void updateObject_rewritesAStoredSession() throws Exception { + when(redisConnection.get("key".getBytes())).thenReturn("value".getBytes()); + + assertEquals("key", redisStorage.updateObject("key", "new value", 60)); + verify(redisConnection).set(any(), any(), any(), any()); + } + + @Test + @DisplayName("updating a session that is not stored fails") + void updateObject_failsWhenTheSessionIsNotStored() { + when(redisConnection.get("key".getBytes())).thenReturn(null); + + assertThrows(RedisSessionException.class, () -> redisStorage.updateObject("key", "new value", 60)); + } + + @Test + @DisplayName("caching a user's roles replaces whatever was cached before") + void cacheUserRoles_replacesTheCachedRoles() { + when(redisTemplate.opsForList()).thenReturn(listOperations); + + redisStorage.cacheUserRoles(7L, List.of("ROLE_NURSE")); + + verify(redisTemplate).delete("roles:7"); + verify(listOperations).rightPushAll("roles:7", List.of("ROLE_NURSE")); + } + + @Test + @DisplayName("a cache write that fails is not propagated") + void cacheUserRoles_swallowsAFailedCacheWrite() { + when(redisTemplate.opsForList()).thenThrow(new RuntimeException("redis down")); + + redisStorage.cacheUserRoles(7L, List.of("ROLE_NURSE")); + } + + @Test + @DisplayName("cached roles are read back for the user") + void getUserRoleFromCache_readsTheCachedRoles() { + when(redisTemplate.opsForList()).thenReturn(listOperations); + when(listOperations.range("roles:7", 0, -1)).thenReturn(List.of("ROLE_NURSE")); + + assertEquals(List.of("ROLE_NURSE"), redisStorage.getUserRoleFromCache(7L)); + } + + @Test + @DisplayName("a cache read that fails reports no roles") + void getUserRoleFromCache_reportsNoRolesWhenTheCacheIsUnreachable() { + when(redisTemplate.opsForList()).thenThrow(new RuntimeException("redis down")); + + assertNull(redisStorage.getUserRoleFromCache(7L)); + } + + @Test + @DisplayName("a Redis session failure carries its message and cause") + void redisSessionException_carriesItsMessageAndCause() { + assertEquals("boom", new RedisSessionException("boom").getMessage()); + assertEquals("boom", new RedisSessionException("boom", new RuntimeException("cause")).getMessage()); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/response/OutputResponseTest.java b/src/test/java/com/iemr/mmu/utils/response/OutputResponseTest.java new file mode 100644 index 00000000..38657cbe --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/response/OutputResponseTest.java @@ -0,0 +1,124 @@ +/* +* 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.mmu.utils.response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.ConnectException; +import java.sql.SQLException; +import java.text.ParseException; + +import org.json.JSONException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.mmu.utils.exception.IEMRException; + +class OutputResponseTest { + + @Test + @DisplayName("a JSON object response is passed through as data") + void setResponse_keepsAJsonObjectAsData() { + OutputResponse response = new OutputResponse(); + response.setResponse("{\"name\":\"Asha\"}"); + + assertTrue(response.isSuccess()); + assertEquals(OutputResponse.SUCCESS, response.getStatusCode()); + assertEquals("Success", response.getErrorMessage()); + assertEquals("{\"name\":\"Asha\"}", response.getData()); + } + + @Test + @DisplayName("a JSON array response is passed through as data") + void setResponse_keepsAJsonArrayAsData() { + OutputResponse response = new OutputResponse(); + response.setResponse("[{\"name\":\"Asha\"}]"); + + assertTrue(response.getData().startsWith("[")); + } + + @Test + @DisplayName("a plain message response is wrapped in a response object") + void setResponse_wrapsAPlainMessage() { + OutputResponse response = new OutputResponse(); + response.setResponse("Data saved successfully"); + + assertTrue(response.getData().contains("Data saved successfully")); + assertTrue(response.getData().contains("response")); + } + + @Test + @DisplayName("each known exception type maps to its own status code") + void setError_mapsEachKnownExceptionToItsStatusCode() { + assertEquals(OutputResponse.USERID_FAILURE, errorFor(new IEMRException("bad user"))); + assertEquals(OutputResponse.OBJECT_FAILURE, errorFor(new JSONException("bad json"))); + assertEquals(OutputResponse.CODE_EXCEPTION, errorFor(new SQLException("bad sql"))); + assertEquals(OutputResponse.CODE_EXCEPTION, errorFor(new ParseException("bad date", 0))); + assertEquals(OutputResponse.CODE_EXCEPTION, errorFor(new NullPointerException("npe"))); + assertEquals(OutputResponse.ENVIRONMENT_EXCEPTION, errorFor(new IOException("io"))); + assertEquals(OutputResponse.ENVIRONMENT_EXCEPTION, errorFor(new ConnectException("refused"))); + assertEquals(OutputResponse.GENERIC_FAILURE, errorFor(new RuntimeException("boom"))); + } + + private int errorFor(Throwable thrown) { + OutputResponse response = new OutputResponse(); + response.setError(thrown); + return response.getStatusCode(); + } + + @Test + @DisplayName("an explicit error carries its own code, message and status") + void setError_carriesTheGivenCodeAndMessage() { + OutputResponse response = new OutputResponse(); + response.setError(404, "Not found", "MISSING"); + + assertEquals(404, response.getStatusCode()); + assertEquals("Not found", response.getErrorMessage()); + assertEquals("MISSING", response.getStatus()); + assertFalse(response.isSuccess()); + } + + @Test + @DisplayName("an error without a status reuses the message as the status") + void setError_reusesTheMessageAsTheStatus() { + OutputResponse response = new OutputResponse(); + response.setError(400, "Bad request"); + + assertEquals("Bad request", response.getStatus()); + } + + @Test + @DisplayName("a failed response carries no data") + void getData_returnsNothingForAFailedResponse() { + assertNull(new OutputResponse().getData()); + } + + @Test + @DisplayName("serialising with nulls keeps the empty data field") + void toStringWithSerialization_keepsTheEmptyDataField() { + assertTrue(new OutputResponse().toStringWithSerialization().contains("\"data\":null")); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/sessionobject/SessionObjectTest.java b/src/test/java/com/iemr/mmu/utils/sessionobject/SessionObjectTest.java new file mode 100644 index 00000000..1e8daa00 --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/sessionobject/SessionObjectTest.java @@ -0,0 +1,110 @@ +/* +* 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.mmu.utils.sessionobject; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.iemr.mmu.utils.redis.RedisSessionException; +import com.iemr.mmu.utils.redis.RedisStorage; + +class SessionObjectTest { + + private RedisStorage objectStore; + private SessionObject sessionObject; + + @BeforeEach + void setUp() { + objectStore = mock(RedisStorage.class); + sessionObject = new SessionObject(); + sessionObject.setObjectStore(objectStore); + } + + @Test + @DisplayName("reading a session delegates to the store with the configured expiry") + void getSessionObject_readsFromTheStore() throws Exception { + when(objectStore.getObject(anyString(), anyInt())).thenReturn("{}"); + + assertEquals("{}", sessionObject.getSessionObject("key")); + } + + @Test + @DisplayName("writing a session delegates to the store") + void setSessionObject_writesToTheStore() throws Exception { + when(objectStore.setObject(anyString(), anyString(), anyInt())).thenReturn("key"); + + assertEquals("key", sessionObject.setSessionObject("key", "{}")); + } + + @Test + @DisplayName("updating a session also refreshes the concurrent-session entry for the user") + void updateSessionObject_refreshesTheConcurrentSessionEntry() throws Exception { + when(objectStore.updateObject(anyString(), anyString(), anyInt())).thenReturn("key"); + + assertEquals("key", sessionObject.updateSessionObject("key", "{\"userName\":\" Nurse1 \"}")); + verify(objectStore).updateObject(org.mockito.ArgumentMatchers.eq("nurse1"), + org.mockito.ArgumentMatchers.eq("key"), anyInt()); + } + + @Test + @DisplayName("updating a session with no user name only refreshes the session itself") + void updateSessionObject_skipsTheConcurrentSessionEntryWithoutAUserName() throws Exception { + when(objectStore.updateObject(anyString(), anyString(), anyInt())).thenReturn("key"); + + assertEquals("key", sessionObject.updateSessionObject("key", "{}")); + verify(objectStore, never()).updateObject(org.mockito.ArgumentMatchers.eq("nurse1"), anyString(), anyInt()); + } + + @Test + @DisplayName("an unparseable session value does not stop the update") + void updateSessionObject_toleratesAnUnparseableValue() throws Exception { + when(objectStore.updateObject(anyString(), anyString(), anyInt())).thenReturn("key"); + + assertEquals("key", sessionObject.updateSessionObject("key", "not-json")); + } + + @Test + @DisplayName("deleting a session delegates to the store") + void deleteSessionObject_deletesFromTheStore() throws Exception { + sessionObject.deleteSessionObject("key"); + + verify(objectStore).deleteObject("key"); + } + + @Test + @DisplayName("a store failure is passed on to the caller") + void getSessionObject_passesOnAStoreFailure() throws Exception { + when(objectStore.getObject(anyString(), anyInt())).thenThrow(new RedisSessionException("redis down")); + + assertThrows(RedisSessionException.class, () -> sessionObject.getSessionObject("key")); + } +} diff --git a/src/test/java/com/iemr/mmu/utils/validator/ValidatorTest.java b/src/test/java/com/iemr/mmu/utils/validator/ValidatorTest.java new file mode 100644 index 00000000..55d7d4df --- /dev/null +++ b/src/test/java/com/iemr/mmu/utils/validator/ValidatorTest.java @@ -0,0 +1,137 @@ +/* +* 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.mmu.utils.validator; + +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.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import com.iemr.mmu.utils.exception.IEMRException; +import com.iemr.mmu.utils.redis.RedisSessionException; +import com.iemr.mmu.utils.sessionobject.SessionObject; + +class ValidatorTest { + + private SessionObject sessionObject; + private Validator validator; + + @BeforeEach + void setUp() { + sessionObject = mock(SessionObject.class); + validator = new Validator(); + validator.setSessionObject(sessionObject); + } + + private JSONObject loginResponse(String ipAddress) throws Exception { + JSONObject response = new JSONObject(); + response.put("loginIPAddress", ipAddress); + return response; + } + + @Test + @DisplayName("a fresh login stores the session and reports success") + void updateCacheObj_storesTheSessionOfAFreshLogin() throws Exception { + when(sessionObject.getSessionObject("key")).thenReturn(null); + + JSONObject result = validator.updateCacheObj(loginResponse("10.0.0.1"), "key", "ipKey"); + + assertEquals("key", result.get("key")); + assertEquals("login success", result.get("sessionStatus")); + verify(sessionObject).setSessionObject(anyString(), anyString()); + } + + @Test + @DisplayName("a session store that is unreachable still reports the login outcome") + void updateCacheObj_reportsTheLoginOutcomeWhenTheStoreIsUnreachable() throws Exception { + when(sessionObject.getSessionObject("key")).thenThrow(new RedisSessionException("redis down")); + + JSONObject result = validator.updateCacheObj(loginResponse("10.0.0.1"), "key", "ipKey"); + + assertEquals("login success", result.get("sessionStatus")); + } + + @Test + @DisplayName("a login from a second IP is reported when IP validation is on") + void updateCacheObj_reportsALoginFromASecondIpAddress() throws Exception { + ReflectionTestUtils.setField(Validator.class, "enableIPValidation", true); + try { + when(sessionObject.getSessionObject("key")) + .thenReturn(new JSONObject().put("loginIPAddress", "10.0.0.9").toString()); + + JSONObject result = validator.updateCacheObj(loginResponse("10.0.0.1"), "key", "ipKey"); + + assertTrue(((String) result.get("sessionStatus")).contains("10.0.0.9")); + } finally { + ReflectionTestUtils.setField(Validator.class, "enableIPValidation", false); + } + } + + @Test + @DisplayName("the stored session is read back for a key") + void getSessionObject_readsTheStoredSession() throws Exception { + when(sessionObject.getSessionObject("key")).thenReturn("{}"); + + assertEquals("{}", validator.getSessionObject("key")); + } + + @Test + @DisplayName("a stored session passes the key check") + void checkKeyExists_acceptsAStoredSession() throws Exception { + when(sessionObject.getSessionObject("key")) + .thenReturn(new JSONObject().put("loginIPAddress", "10.0.0.1").toString()); + + validator.checkKeyExists("key", "10.0.0.1"); + } + + @Test + @DisplayName("a session that is not stored fails the key check") + void checkKeyExists_rejectsASessionThatIsNotStored() throws Exception { + when(sessionObject.getSessionObject("key")).thenReturn(null); + + IEMRException thrown = assertThrows(IEMRException.class, () -> validator.checkKeyExists("key", "10.0.0.1")); + assertEquals("Session is expired. Please login again.", thrown.getMessage()); + } + + @Test + @DisplayName("a session opened from another IP fails the key check when IP validation is on") + void checkKeyExists_rejectsASessionFromAnotherIpAddress() throws Exception { + ReflectionTestUtils.setField(Validator.class, "enableIPValidation", true); + try { + when(sessionObject.getSessionObject("key")) + .thenReturn(new JSONObject().put("loginIPAddress", "10.0.0.9").toString()); + + assertThrows(IEMRException.class, () -> validator.checkKeyExists("key", "10.0.0.1")); + } finally { + ReflectionTestUtils.setField(Validator.class, "enableIPValidation", false); + } + } +} From 65dcad84d40d441aa0b41f38e6df41f335eed16b Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 6 Nov 2026 17:43:45 +0530 Subject: [PATCH 3/3] chore: set MMU-API version to 3.13.0 Co-Authored-By: Claude Opus 5.5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5749c203..04e4e450 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.mmu mmu-api - 3.12.0 + 3.13.0 war MMU-API