Java tutorial
/** * The contents of this file are subject to the OpenMRS Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.module.auditlog; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.openmrs.module.auditlog.AuditLog.Action.CREATED; import static org.openmrs.module.auditlog.AuditLog.Action.DELETED; import static org.openmrs.module.auditlog.AuditLog.Action.UPDATED; import static org.openmrs.module.auditlog.util.AuditLogConstants.SEPARATOR; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.junit.Test; import org.openmrs.Concept; import org.openmrs.ConceptClass; import org.openmrs.ConceptComplex; import org.openmrs.ConceptDatatype; import org.openmrs.ConceptName; import org.openmrs.ConceptNumeric; import org.openmrs.DrugOrder; import org.openmrs.EncounterType; import org.openmrs.Location; import org.openmrs.Order; import org.openmrs.PatientIdentifierType; import org.openmrs.api.APIException; import org.openmrs.api.ConceptService; import org.openmrs.api.EncounterService; import org.openmrs.api.PatientService; import org.openmrs.api.context.Context; import org.openmrs.module.auditlog.AuditLog.Action; import org.openmrs.module.auditlog.strategy.AuditStrategy; import org.openmrs.module.auditlog.strategy.ExceptionBasedAuditStrategy; import org.openmrs.module.auditlog.util.AuditLogConstants; import org.openmrs.module.auditlog.util.AuditLogUtil; import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.test.annotation.NotTransactional; /** * Contains tests for testing the core functionality of the module */ @SuppressWarnings("deprecation") public class AuditLogBehaviorTest extends BaseBehaviorTest { @Test @NotTransactional public void shouldCreateAnAuditLogEntryWhenANewObjectIsCreated() { Concept concept = new Concept(); ConceptName cn = new ConceptName("new", Locale.ENGLISH); cn.setConcept(concept); concept.addName(cn); concept.setDatatype(conceptService.getConceptDatatype(4)); concept.setConceptClass(conceptService.getConceptClass(4)); conceptService.saveConcept(concept); List<AuditLog> logs = getAllLogs(); assertNotNull(concept.getConceptId()); //Should have created an entry for the concept and concept name assertEquals(2, logs.size()); //The latest logs come first assertEquals(CREATED, logs.get(0).getAction()); assertEquals(CREATED, logs.get(1).getAction()); } @Test @NotTransactional public void shouldCreateAnAuditLogEntryWhenAnObjectIsDeleted() throws Exception { EncounterType encounterType = encounterService.getEncounterType(6); encounterService.purgeEncounterType(encounterType); List<AuditLog> logs = getAllLogs(encounterType.getId(), EncounterType.class, null); //Should have created a log entry for deleted Encounter type assertEquals(1, logs.size()); AuditLog al = logs.get(0); assertEquals(DELETED, al.getAction()); assertNull(al.getSerializedData()); } @Test @NotTransactional public void shouldStoreTheLastStateOfAsDeletedObjectIfTheFeatureIsEnabled() throws Exception { AuditLogUtil.setGlobalProperty(AuditLogConstants.GP_STORE_LAST_STATE_OF_DELETED_ITEMS, "true"); EncounterType encounterType = encounterService.getEncounterType(6); encounterService.purgeEncounterType(encounterType); List<AuditLog> logs = getAllLogs(encounterType.getId(), EncounterType.class, null); //Should have created a log entry for deleted Encounter type assertEquals(1, logs.size()); AuditLog al = logs.get(0); assertEquals(DELETED, al.getAction()); String serializedData = AuditLogUtil.getAsString(al.getSerializedData()); assertEquals( "{\"encounterTypeId\":6," + "\"retireReason\":\"for testing\"," + "\"retiredBy\":\"1\"," + "\"description\":\"Visit to the laboratory\"," + "\"name\":\"Laboratory\"," + "\"retired\":\"true\"," + "\"dateRetired\":\"2008-08-15 00:00:00\"," + "\"dateCreated\":\"2008-08-15 15:39:55\"," + "\"uuid\":\"02c533ab-b74b-4ee4-b6e5-ffb6d09a0ac8\"," + "\"creator\":\"1\"}", serializedData); } @Test @NotTransactional public void shouldCreateAnAuditLogEntryWhenAnObjectIsEdited() throws Exception { Concept concept = conceptService.getConcept(3); Integer oldConceptClassId = concept.getConceptClass().getId(); Integer oldDatatypeId = concept.getDatatype().getId(); ConceptClass cc = conceptService.getConceptClass(2); ConceptDatatype dt = conceptService.getConceptDatatype(3); String oldVersion = concept.getVersion(); String newVersion = "1.11"; assertFalse(cc.equals(concept.getConceptClass())); assertFalse(dt.equals(concept.getDatatype())); assertFalse(newVersion.equalsIgnoreCase(oldVersion)); concept.setConceptClass(cc); concept.setDatatype(dt); concept.setVersion(newVersion); conceptService.saveConcept(concept); List<AuditLog> logs = getAllLogs(); //Should have created a log entry for edited concept assertEquals(1, logs.size()); AuditLog auditLog = logs.get(0); //Should have created entries for the changes properties and their old values assertEquals(UPDATED, auditLog.getAction()); //Check that there are 3 property tag entries Map<String, List> changes = AuditLogUtil.getChangesOfUpdatedItem(auditLog); assertEquals(3, changes.size()); assertEquals(oldConceptClassId.toString(), AuditLogUtil.getPreviousValueOfUpdatedItem("conceptClass", auditLog)); assertEquals(oldDatatypeId.toString(), AuditLogUtil.getPreviousValueOfUpdatedItem("datatype", auditLog)); assertEquals(oldVersion, AuditLogUtil.getPreviousValueOfUpdatedItem("version", auditLog)); assertEquals(cc.getId().toString(), AuditLogUtil.getNewValueOfUpdatedItem("conceptClass", auditLog)); assertEquals(dt.getId().toString(), AuditLogUtil.getNewValueOfUpdatedItem("datatype", auditLog)); assertEquals(newVersion, AuditLogUtil.getNewValueOfUpdatedItem("version", auditLog)); } @Test @NotTransactional public void shouldCreateNoLogEntryIfNoChangesAreMadeToAnExistingObject() throws Exception { EncounterType encounterType = encounterService.getEncounterType(2); encounterService.saveEncounterType(encounterType); assertTrue(getAllLogs().isEmpty()); } @Test @NotTransactional public void shouldIgnoreDateChangedAndCreatedFields() throws Exception { Concept concept = conceptService.getConcept(3); //sanity checks assertNull(concept.getDateChanged()); assertNull(concept.getChangedBy()); concept.setDateChanged(new Date()); concept.setChangedBy(Context.getAuthenticatedUser()); conceptService.saveConcept(concept); assertTrue(getAllLogs().isEmpty()); } @Test @NotTransactional public void shouldHandleInsertsOrUpdatesOrDeletesInEachTransactionIndependently() throws InterruptedException { final int N = 50; final Set<Thread> threads = new LinkedHashSet<Thread>(); for (int i = 0; i < N; i++) { threads.add(new Thread(new Runnable() { @Override public void run() { try { Context.openSession(); Context.authenticate("admin", "test"); Integer index = new Integer(Thread.currentThread().getName()); EncounterService es = Context.getEncounterService(); if (index == 0) { //Let's have a delete EncounterType existingEncounterType = es.getEncounterType(6); assertNotNull(existingEncounterType); es.purgeEncounterType(existingEncounterType); } else { EncounterType encounterType; if (index % 2 == 0) { //And some updates encounterType = es.getEncounterType(2); encounterType.setDescription("New Description-" + index); } else { //And some new rows inserted encounterType = new EncounterType("Encounter Type-" + index, "Description-" + index); } es.saveEncounterType(encounterType); } } finally { Context.closeSession(); } } }, Integer.toString(i))); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } assertEquals(N, getAllLogs().size()); List<Action> actions = new ArrayList<Action>(); actions.add(CREATED);//should match expected count of created log entries assertEquals(25, auditLogService.getAuditLogs(null, actions, null, null, false, null, null).size()); actions.clear(); actions.add(UPDATED);//should match expected count of updated log entries assertEquals(24, auditLogService.getAuditLogs(null, actions, null, null, false, null, null).size()); actions.clear(); actions.add(DELETED);//should match expected count of deleted log entries assertEquals(1, auditLogService.getAuditLogs(null, actions, null, null, false, null, null).size()); } @Test @NotTransactional public void shouldNotCreateAuditLogsForUnAuditedObjects() { assertFalse(auditLogService.isAudited(Location.class)); Location location = new Location(); location.setName("najja"); location.setAddress1("test address"); Location savedLocation = Context.getLocationService().saveLocation(location); assertNotNull(savedLocation.getLocationId());//sanity check that it was actually created //Should not have created any logs assertTrue(getAllLogs().isEmpty()); } @Test @NotTransactional public void shouldIgnoreChangesForStringFieldsFromNullToBlank() throws Exception { PatientService ps = Context.getPatientService(); PatientIdentifierType idType = ps.getPatientIdentifierType(1); idType.setFormat(null); ps.savePatientIdentifierType(idType); int originalLogCount = getAllLogs().size(); idType.setFormat(""); ps.savePatientIdentifierType(idType); assertEquals(originalLogCount, getAllLogs().size()); } @Test @NotTransactional public void shouldIgnoreChangesForStringFieldsFromBlankToNull() throws Exception { PatientService ps = Context.getPatientService(); PatientIdentifierType idType = ps.getPatientIdentifierType(1); idType.setFormat(""); idType = ps.savePatientIdentifierType(idType); //this will fail when required version is 1.9 since it converts blanks to null assertEquals("", idType.getFormat()); int originalLogCount = getAllLogs().size(); idType.setFormat(null); ps.savePatientIdentifierType(idType); assertEquals(originalLogCount, getAllLogs().size()); } @Test @NotTransactional public void shouldBeCaseInsensitiveForChangesInStringFields() throws Exception { PatientService ps = Context.getPatientService(); PatientIdentifierType idType = ps.getPatientIdentifierType(1); idType.setFormat("test"); idType = ps.savePatientIdentifierType(idType); int originalLogCount = getAllLogs().size(); idType.setFormat("TEST"); ps.savePatientIdentifierType(idType); assertEquals(originalLogCount, getAllLogs().size()); } @Test @NotTransactional public void shouldAuditAnyObjectWhenStrategyIsSetToAll() throws Exception { assertFalse(auditLogService.isAudited(Location.class)); setAuditConfiguration(AuditStrategy.ALL, null, false); assertTrue(auditLogService.isAudited(Location.class)); Location location = new Location(); location.setName("new location"); Context.getLocationService().saveLocation(location); assertEquals(1, getAllLogs(location.getId(), Location.class, Collections.singletonList(CREATED)).size()); } @Test @NotTransactional public void shouldNotAuditAnyObjectWhenStrategyIsSetToNone() throws Exception { assertTrue(auditLogService.isAudited(EncounterType.class)); setAuditConfiguration(AuditStrategy.NONE, null, false); assertFalse(auditLogService.isAudited(EncounterType.class)); EncounterType encounterType = encounterService.getEncounterType(6); encounterService.purgeEncounterType(encounterType); assertEquals(0, getAllLogs().size()); } @Test @NotTransactional public void shouldNotCreateLogWhenStrategyIsSetToAllExceptAndObjectTypeIsListedAsExcluded() throws Exception { Class<?> type = EncounterType.class; setAuditConfiguration(AuditStrategy.ALL_EXCEPT, type.getName(), false); assertFalse(auditLogService.isAudited(type)); assertTrue(helper.getExceptions().contains(type)); EncounterType encounterType = encounterService.getEncounterType(6); encounterService.purgeEncounterType(encounterType); assertEquals(0, getAllLogs(encounterType.getId(), EncounterType.class, Collections.singletonList(DELETED)).size()); } @Test @NotTransactional public void shouldCreateLogWhenStrategyIsSetToAllExceptAndObjectTypeIsNotListedAsAnException() throws Exception { setAuditConfiguration(AuditStrategy.ALL_EXCEPT, EncounterType.class.getName(), false); Location location = new Location(); location.setName("new location"); Context.getLocationService().saveLocation(location); assertEquals(1, getAllLogs(location.getId(), Location.class, Collections.singletonList(CREATED)).size()); } @Test public void shouldUpdateTheAuditedClassCacheWhenTheAuditedClassGlobalPropertyIsUpdatedWithAnAddition() throws Exception { assertFalse(auditLogService.isAudited(Order.class)); assertFalse(auditLogService.isAudited(DrugOrder.class)); Set<Class<?>> auditedClasses = new HashSet<Class<?>>(); auditedClasses.addAll(helper.getExceptions()); auditedClasses.add(Order.class); String exceptions = StringUtils.join(AuditLogUtil.getAsListOfClassnames(auditedClasses), SEPARATOR); AuditLogUtil.setGlobalProperty(ExceptionBasedAuditStrategy.GLOBAL_PROPERTY_EXCEPTION, exceptions); assertTrue(auditLogService.isAudited(Order.class)); assertTrue(auditLogService.isAudited(DrugOrder.class)); } @Test public void shouldUpdateTheAuditedClassCacheWhenTheAuditedClassGlobalPropertyIsUpdatedWithARemoval() throws Exception { assertTrue(auditLogService.isAudited(Concept.class)); assertTrue(auditLogService.isAudited(ConceptNumeric.class)); assertTrue(auditLogService.isAudited(ConceptComplex.class)); Set<Class<?>> auditedClasses = new HashSet<Class<?>>(); auditedClasses.addAll(helper.getExceptions()); auditedClasses.remove(Concept.class); String exceptions = StringUtils.join(AuditLogUtil.getAsListOfClassnames(auditedClasses), SEPARATOR); AuditLogUtil.setGlobalProperty(ExceptionBasedAuditStrategy.GLOBAL_PROPERTY_EXCEPTION, exceptions); assertFalse(auditLogService.isAudited(Concept.class)); assertTrue(auditLogService.isAudited(ConceptNumeric.class)); assertTrue(auditLogService.isAudited(ConceptComplex.class)); } @Test @NotTransactional public void shouldNotCreateAnAuditLogWhenTheTransactionIsRolledBack() throws Exception { startAuditing(ConceptClass.class); assertTrue(auditLogService.isAudited(ConceptClass.class)); ConceptService cs = Context.getConceptService(); int initialLogCount = getAllLogs().size(); boolean exceptionThrown = false; try { ConceptClass cc = cs.getConceptClass(1); cc.setUuid("An invalid long uuid that for sure should result into an exception"); cs.saveConceptClass(cc); } catch (UncategorizedSQLException e) { exceptionThrown = true; } assertTrue(exceptionThrown); //No log should have been created assertEquals(initialLogCount, getAllLogs().size()); } @Test @NotTransactional public void shouldCreateLogsForActionsSavedInNestedTransactions() throws Exception { startAuditing(Location.class); try { assertEquals(true, auditLogService.isAudited(Location.class)); final String newLocationName = "Some strange new name"; Location location = Context.getLocationService().getLocation(1); //sanity checks List<AuditLog> locationLogs = getAllLogs(location.getId(), Location.class, Collections.singletonList(UPDATED)); assertEquals(0, locationLogs.size()); EncounterType et = Context.getEncounterService().getEncounterType(MockNestedService.ENCOUNTER_TYPE_ID); List<AuditLog> encounterTypeLogs = getAllLogs(et.getId(), EncounterType.class, Collections.singletonList(UPDATED)); assertEquals(0, encounterTypeLogs.size()); assertEquals(false, location.getName().equalsIgnoreCase(newLocationName)); location.setName(newLocationName); Context.getService(MockNestedService.class).outerTransaction(location, false, false); locationLogs = getAllLogs(location.getId(), Location.class, Collections.singletonList(UPDATED)); assertEquals(1, locationLogs.size()); assertEquals(UPDATED, locationLogs.get(0).getAction()); encounterTypeLogs = getAllLogs(et.getId(), EncounterType.class, Collections.singletonList(UPDATED)); assertEquals(1, encounterTypeLogs.size()); assertEquals(UPDATED, encounterTypeLogs.get(0).getAction()); } finally { stopAuditing(Location.class); } assertEquals(false, auditLogService.isAudited(Location.class)); } @Test @NotTransactional public void shouldNotCreateLogsForActionsSavedInInnerTransactionIfRollback() throws Exception { startAuditing(Location.class); assertEquals(true, auditLogService.isAudited(Location.class)); assertEquals(true, auditLogService.isAudited(EncounterType.class)); final String newLocationName = "Some strange new name"; Location location = Context.getLocationService().getLocation(1); //sanity checks List<AuditLog> locationLogs = getAllLogs(location.getId(), Location.class, Collections.singletonList(UPDATED)); assertEquals(0, locationLogs.size()); EncounterType et = Context.getEncounterService().getEncounterType(MockNestedService.ENCOUNTER_TYPE_ID); List<AuditLog> encounterTypeLogs = getAllLogs(et.getId(), EncounterType.class, Collections.singletonList(UPDATED)); assertEquals(0, encounterTypeLogs.size()); assertEquals(false, location.getName().equalsIgnoreCase(newLocationName)); location.setName(newLocationName); try { Context.getService(MockNestedService.class).outerTransaction(location, true, false); } catch (APIException e) { } encounterTypeLogs = getAllLogs(et.getId(), EncounterType.class, Collections.singletonList(UPDATED)); assertEquals(0, encounterTypeLogs.size()); locationLogs = getAllLogs(location.getId(), Location.class, Collections.singletonList(UPDATED)); assertEquals(1, locationLogs.size()); assertEquals(UPDATED, locationLogs.get(0).getAction()); } @Test @NotTransactional public void shouldNotCreateLogsForActionsSavedInOuterTransactionIfRollback() throws Exception { startAuditing(Location.class); assertTrue(auditLogService.isAudited(Location.class)); assertTrue(auditLogService.isAudited(EncounterType.class)); final String newLocationName = "Some strange new name"; Location location = Context.getLocationService().getLocation(1); //sanity checks List<AuditLog> locationLogs = getAllLogs(location.getId(), Location.class, Collections.singletonList(UPDATED)); assertEquals(0, locationLogs.size()); EncounterType et = Context.getEncounterService().getEncounterType(MockNestedService.ENCOUNTER_TYPE_ID); List<AuditLog> encounterTypeLogs = getAllLogs(et.getId(), EncounterType.class, Collections.singletonList(UPDATED)); assertEquals(0, encounterTypeLogs.size()); assertEquals(false, location.getName().equalsIgnoreCase(newLocationName)); location.setName(newLocationName); try { Context.getService(MockNestedService.class).outerTransaction(location, false, true); } catch (APIException e) { } locationLogs = getAllLogs(location.getId(), Location.class, Collections.singletonList(UPDATED)); assertEquals(0, locationLogs.size()); encounterTypeLogs = getAllLogs(et.getId(), EncounterType.class, Collections.singletonList(UPDATED)); assertEquals(1, encounterTypeLogs.size()); assertEquals(UPDATED, encounterTypeLogs.get(0).getAction()); } @Test @NotTransactional public void shouldNotCreateLogsForActionsSavedInBothTransactionsIfBothRollbacked() throws Exception { startAuditing(Location.class); assertTrue(auditLogService.isAudited(Location.class)); assertTrue(auditLogService.isAudited(EncounterType.class)); final String newLocationName = "Some strange new name"; Location location = Context.getLocationService().getLocation(1); //sanity checks List<AuditLog> locationLogs = getAllLogs(location.getId(), Location.class, Collections.singletonList(UPDATED)); assertEquals(0, locationLogs.size()); EncounterType et = Context.getEncounterService().getEncounterType(MockNestedService.ENCOUNTER_TYPE_ID); List<AuditLog> encounterTypeLogs = getAllLogs(et.getId(), EncounterType.class, Collections.singletonList(UPDATED)); assertEquals(0, encounterTypeLogs.size()); assertEquals(false, location.getName().equalsIgnoreCase(newLocationName)); location.setName(newLocationName); try { Context.getService(MockNestedService.class).outerTransaction(location, true, true); } catch (APIException e) { } locationLogs = getAllLogs(location.getId(), Location.class, Collections.singletonList(UPDATED)); assertEquals(0, locationLogs.size()); encounterTypeLogs = getAllLogs(et.getId(), EncounterType.class, Collections.singletonList(UPDATED)); assertEquals(0, encounterTypeLogs.size()); } @Test @NotTransactional public void shouldNotCreateLogIfADetachedObjectIsSavedWithNoChanges() throws Exception { assertTrue(auditLogService.isAudited(EncounterType.class)); EncounterService ls = Context.getEncounterService(); EncounterType type = ls.getEncounterType(1); //sanity checks List<AuditLog> logs = getAllLogs(type.getId(), EncounterType.class, null); assertEquals(0, logs.size()); Context.evictFromSession(type); ls.saveEncounterType(type); logs = getAllLogs(type.getId(), EncounterType.class, null); assertEquals(0, logs.size()); } @Test @NotTransactional public void shouldCreateLogIfADetachedObjectIsSavedWithChanges() throws Exception { assertTrue(auditLogService.isAudited(EncounterType.class)); EncounterService ls = Context.getEncounterService(); EncounterType type = ls.getEncounterType(1); //sanity checks List<AuditLog> logs = getAllLogs(type.getId(), EncounterType.class, null); assertEquals(0, logs.size()); Context.evictFromSession(type); final String newName = "new name"; assertFalse(newName.equals(type.getName())); final String oldName = type.getName(); type.setName(newName); ls.saveEncounterType(type); logs = getAllLogs(type.getId(), EncounterType.class, null); assertEquals(1, logs.size()); logs = getAllLogs(type.getId(), EncounterType.class, Collections.singletonList(UPDATED)); assertEquals(1, logs.size()); AuditLog log = logs.get(0); //Check that there is one property tag entry Map<String, List> changes = AuditLogUtil.getChangesOfUpdatedItem(log); assertEquals(1, changes.size()); assertEquals(oldName, AuditLogUtil.getPreviousValueOfUpdatedItem("name", log)); assertEquals(newName, AuditLogUtil.getNewValueOfUpdatedItem("name", log)); } }