/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.module.bc.document.service; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.SortedSet; import org.kuali.kfs.fp.businessobject.FiscalYearFunctionControl; import org.kuali.kfs.module.bc.BCConstants; import org.kuali.kfs.module.bc.BCConstants.LockStatus; import org.kuali.kfs.module.bc.businessobject.BudgetConstructionFundingLock; import org.kuali.kfs.module.bc.businessobject.BudgetConstructionHeader; import org.kuali.kfs.module.bc.businessobject.BudgetConstructionLockStatus; import org.kuali.kfs.module.bc.businessobject.BudgetConstructionPosition; import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding; import org.kuali.kfs.module.bc.document.dataaccess.BudgetConstructionDao; import org.kuali.kfs.sys.ConfigureContext; import org.kuali.kfs.sys.KFSConstants.BudgetConstructionConstants; import org.kuali.kfs.sys.KFSPropertyConstants; import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeader; import org.kuali.kfs.sys.context.KualiTestBase; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.rice.krad.service.BusinessObjectService; /** * This class tests the Lock Service */ @ConfigureContext public class LockServiceTest extends KualiTestBase { private LockService lockService; private BudgetConstructionDao bcHeaderDao; private FinancialSystemDocumentHeader docHeader; private BudgetConstructionHeader bcHeader; private BudgetConstructionHeader bcHeaderTwo; private BudgetConstructionPosition bcPosition; private PendingBudgetConstructionAppointmentFunding bcAFunding; private BudgetConstructionLockStatus bcLockStatus; private LockStatus lockStatus; private SortedSet<BudgetConstructionFundingLock> fundingLocks; Iterator<BudgetConstructionFundingLock> fundingIter; private BudgetConstructionFundingLock fundingLock; /* * these values are filled in from the database, taking the first row that comes along. * these fields are also static, so we don't have to return to the database for each of the tests. * therefore, it follows that deleting data from the database while the tests are running could result * in failed tests, even though nothing is wrong with the code itself. */ private String fdocNumber; private String chartOfAccountsCode; private String accountNumber; private String subAccountNumber; private String financialObjectCode; private String financialSubObjectCode; private String emplid; private Integer universityFiscalYear; private String positionNumber; private String pUIdOne = "3670600494"; // MCGUIRE private String pUIdTwo = "6162502038"; // khuntley // set up some data for the tests. // we will run everything in one test method, so this only needs to be done once @Override public void setUp() throws Exception { super.setUp(); // get the services we need lockService = SpringContext.getBean(LockService.class); bcHeaderDao = SpringContext.getBean(BudgetConstructionDao.class); if (!runTests()) return; // find a test fiscal year universityFiscalYear = setTestFiscalYear(); assertTrue("Unable to obtain fiscal year",universityFiscalYear != 0); System.err.println( "Testing Fiscal Year: " + universityFiscalYear ); // find a test funding row for this fiscal year. assertTrue( "Unable to set test funding", setTestFunding() ); // finally, get the parent document for this funding and position assertTrue( "Unable to set test document number", setTestDocumentNumber() ); } @Override public void tearDown() throws Exception { clearTestRowLocks(); super.tearDown(); } private boolean runTests() { // change this to return false to prevent running tests return false; } @ConfigureContext(shouldCommitTransactions = true) public void testOne() { if (!runTests()) return; // // (the tests below will check that the unlock activity here took effect). clearTestRowLocks(); // trivial account lock/unlock assertFalse("test header was unlocked on initialization", lockService.isAccountLocked(bcHeader)); bcLockStatus = lockService.lockAccount(bcHeader, pUIdOne); assertTrue("account lock attempt on header succeeded", bcLockStatus.getLockStatus() == LockStatus.SUCCESS); assertTrue("test header has an account lock", lockService.isAccountLocked(bcHeader)); bcLockStatus = lockService.lockAccount(bcHeader, pUIdOne); assertTrue("test header locked successfully on second attempt", bcLockStatus.getLockStatus() == LockStatus.SUCCESS); lockService.unlockAccount(bcHeader); assertFalse("unlock attempt on test header succeeded--no lock found", lockService.isAccountLocked(bcHeader)); // account lock attempt with account lock set by other bcLockStatus = lockService.lockAccount(bcHeader, pUIdOne); assertTrue("initial header lock by "+pUIdOne+" succeeded", bcLockStatus.getLockStatus() == LockStatus.SUCCESS); assertTrue("header locked by "+pUIdOne, lockService.isAccountLocked(bcHeader)); // bcHeaderTwo is a different object representing the same budget construction header // it must be fetched AFTER the object is locked bcHeaderTwo = bcHeaderDao.getByCandidateKey(chartOfAccountsCode, accountNumber, subAccountNumber, universityFiscalYear); assertTrue("two objects pointing to the same header have the same account", bcHeaderTwo.getAccountNumber().equals(accountNumber)); bcLockStatus = lockService.lockAccount(bcHeaderTwo, pUIdTwo); assertTrue(pUIdTwo+" could not get a lock on header already locked by "+pUIdOne, bcLockStatus.getLockStatus() == LockStatus.BY_OTHER); assertTrue("lock is owned by "+pUIdOne, bcLockStatus.getAccountLockOwner().equals(pUIdOne)); assertTrue(pUIdTwo+"'s pointer to the test header row shows a lock", lockService.isAccountLocked(bcHeaderTwo)); // funding lock attempt with account lock set in previous test bcLockStatus = lockService.lockFunding(bcHeader, pUIdOne); assertTrue("failed funding lock attempt on a header with an existing acccount lock", bcLockStatus.getLockStatus() == LockStatus.BY_OTHER); assertTrue("no funding locks exist after failed attempt", lockService.getFundingLocks(bcHeader).isEmpty()); // account unlock by other - needs account lock in previous test // this tests opimistic lock exception catch // this configuration of the test must run in a test method that // is annotated as ShouldCommitTransactions lockService.unlockAccount(bcHeaderTwo); assertFalse("account was unlocked successfully",lockService.isAccountLocked(bcHeaderTwo)); assertTrue("unlock with a different object against the same row triggers an OJB optimistic lock exception", lockService.unlockAccount(bcHeader) == LockStatus.OPTIMISTIC_EX); assertFalse("the account is still unlocked", lockService.isAccountLocked(bcHeader)); // trivial funding lock/unlock bcLockStatus = lockService.lockFunding(bcHeader, pUIdOne); assertTrue(pUIdOne+" obtained a funding lock", bcLockStatus.getLockStatus() == LockStatus.SUCCESS); bcLockStatus = lockService.lockFunding(bcHeader, pUIdOne); assertTrue(pUIdOne+"'s re-attempt to fetch the same lock also succeeded", bcLockStatus.getLockStatus() == LockStatus.SUCCESS); bcLockStatus = lockService.lockFunding(bcHeader, pUIdTwo); assertTrue(pUIdTwo+" can also get a funding lock for the same accounting key",bcLockStatus.getLockStatus() == LockStatus.SUCCESS); assertFalse("the set of funding locks for this accounting key is not empty", lockService.getFundingLocks(bcHeader).isEmpty()); fundingLocks = lockService.getFundingLocks(bcHeader); fundingIter = fundingLocks.iterator(); assertTrue(fundingIter.hasNext()); fundingLock = fundingIter.next(); assertTrue("the first funding lock is by accounting key, not by position",fundingLock.getPositionNumber().equals("NotFnd")); assertTrue(fundingIter.hasNext()); fundingLock = fundingIter.next(); assertTrue("the second funding lock is also not by position", fundingLock.getPositionNumber().equals("NotFnd")); lockStatus = lockService.unlockFunding(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear(), pUIdOne); assertTrue(pUIdOne+"'s funding lock was successfully removed", lockStatus == LockStatus.SUCCESS); lockStatus = lockService.unlockFunding(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear(), pUIdTwo); assertTrue(pUIdTwo+"'s funding lock was successfully removed", lockStatus == LockStatus.SUCCESS); assertTrue("there are no remaining funding locks for this accounting key", lockService.getFundingLocks(bcHeader).isEmpty()); // account lock attempt with funding locks set // one funding lock has an associated position lock, the other is an orphan bcLockStatus = lockService.lockPosition(positionNumber, universityFiscalYear, pUIdOne); assertTrue(pUIdOne+" successfully obtained a position lock",bcLockStatus.getLockStatus() == LockStatus.SUCCESS); bcLockStatus = lockService.lockFunding(bcHeader, pUIdOne); assertTrue(pUIdOne+" successfully obtained a funding lock on an account funding the position",bcLockStatus.getLockStatus() == LockStatus.SUCCESS); bcLockStatus = lockService.lockFunding(bcHeader, pUIdTwo); assertTrue(pUIdTwo+" successfully obtained an orphan funding lock--no position lock is involved",bcLockStatus.getLockStatus() == LockStatus.SUCCESS); assertFalse("the funding lock table has rows for the account specified by this header",lockService.getFundingLocks(bcHeader).isEmpty()); bcLockStatus = lockService.lockAccount(bcHeaderTwo, pUIdTwo); assertTrue(pUIdTwo+" cannot lock an accounting key for which this user has a funding lock",bcLockStatus.getLockStatus() == LockStatus.FLOCK_FOUND); assertFalse("there are no funding locks involving this accounting key",bcLockStatus.getFundingLocks().isEmpty()); fundingIter = bcLockStatus.getFundingLocks().iterator(); assertTrue("an orphan funding lock exists",fundingIter.hasNext()); fundingLock = fundingIter.next(); assertTrue("funding lock is marked as an orphan",fundingLock.getPositionNumber().equals("NotFnd")); // orphan assertTrue("a funding lock exists with an associated position",fundingIter.hasNext()); fundingLock = fundingIter.next(); assertTrue(positionNumber+" has a funding lock",fundingLock.getPositionNumber().equals(positionNumber)); // associated position assertFalse(pUIdTwo+" does not have an account lock",lockService.isAccountLocked(bcHeaderTwo)); lockStatus = lockService.unlockFunding(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear(), pUIdOne); assertTrue(pUIdOne+" successfully released a funding lock", lockStatus == LockStatus.SUCCESS); lockStatus = lockService.unlockFunding(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear(), pUIdTwo); assertTrue(pUIdTwo+" successfully released a funding lock",lockStatus == LockStatus.SUCCESS); assertTrue("no funding locks are left",lockService.getFundingLocks(bcHeader).isEmpty()); lockStatus = lockService.unlockPosition(positionNumber, universityFiscalYear); assertTrue(positionNumber+" lock was released successfully", lockStatus == LockStatus.SUCCESS); // trivial position lock/unlock bcLockStatus = lockService.lockPosition(positionNumber, universityFiscalYear, pUIdOne); assertTrue("position lock: lock obtained by "+pUIdOne, bcLockStatus.getLockStatus() == LockStatus.SUCCESS); bcLockStatus = lockService.lockPosition(positionNumber, universityFiscalYear, pUIdOne); assertTrue("position lock: successful re-lock attempt by "+pUIdOne, bcLockStatus.getLockStatus() == LockStatus.SUCCESS); assertTrue("position lock: "+positionNumber+" is locked", lockService.isPositionLocked(positionNumber, universityFiscalYear)); lockStatus = lockService.unlockPosition(positionNumber, universityFiscalYear); assertTrue(lockStatus == LockStatus.SUCCESS); assertFalse("position lock: "+positionNumber+" successfully unlocked", lockService.isPositionLocked(positionNumber, universityFiscalYear)); // position lock attempt with position lock by other bcLockStatus = lockService.lockPosition(positionNumber, universityFiscalYear, pUIdOne); assertTrue("position lock conflict: position lock obtained by "+pUIdOne,bcLockStatus.getLockStatus() == LockStatus.SUCCESS); bcLockStatus = lockService.lockPosition(positionNumber, universityFiscalYear, pUIdTwo); assertTrue("position lock conflict: position lock denied to "+pUIdTwo, bcLockStatus.getLockStatus() == LockStatus.BY_OTHER); assertTrue("position lock conflict: position is still locked",lockService.isPositionLocked(positionNumber, universityFiscalYear)); lockStatus = lockService.unlockPosition(positionNumber, universityFiscalYear); assertTrue("position lock conflict: position lock successfully released", lockStatus == LockStatus.SUCCESS); assertFalse("position lock conflict: no positions locks remain", lockService.isPositionLocked(positionNumber, universityFiscalYear)); // trivial transaction lock/unlock // this test bcHeader, but the application will probably derive the params from BCAppointmentFunding lockService.unlockTransaction(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear()); assertFalse("transaction lock: no current locks", lockService.isTransactionLocked(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear())); bcLockStatus = lockService.lockTransaction(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear(), pUIdOne); assertTrue("transaction lock: obtained by "+pUIdOne, bcLockStatus.getLockStatus() == LockStatus.SUCCESS); assertTrue("transaction lock: in effect", lockService.isTransactionLocked(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear())); lockStatus = lockService.unlockTransaction(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear()); assertTrue("transaction lock: successfully released by "+pUIdOne, lockStatus == LockStatus.SUCCESS); assertFalse("transaction lock: no longer in effect", lockService.isTransactionLocked(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear())); // transaction lock attempt with transaction lock by other // this test uses bcHeader, but the application will probably derive the params from BCAppointmentFunding assertFalse("conflicting transaction lock: current locks", lockService.isTransactionLocked(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear())); bcLockStatus = lockService.lockTransaction(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear(), pUIdOne); assertTrue("conflicting transaction lock: lock obtained by "+pUIdOne, bcLockStatus.getLockStatus() == LockStatus.SUCCESS); assertTrue("conflicting transaction lock: transaction lock is in effect", lockService.isTransactionLocked(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear())); bcLockStatus = lockService.lockTransaction(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear(), pUIdTwo); assertTrue("conflicting transaction lock: "+pUIdTwo+" could not get the same transaction lock", bcLockStatus.getLockStatus() == LockStatus.BY_OTHER); assertTrue("conflicting transaction lock: lock is owned by "+pUIdOne, bcLockStatus.getTransactionLockOwner().equals(pUIdOne)); assertTrue("conflicting transaction lock: still locked by "+pUIdOne,lockService.isTransactionLocked(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear())); lockStatus = lockService.unlockTransaction(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear()); assertTrue("conflicting transaction lock: lock removed by "+pUIdOne, lockStatus == LockStatus.SUCCESS); assertFalse("conflicting transaction lock: transaction locks still exist", lockService.isTransactionLocked(bcHeader.getChartOfAccountsCode(), bcHeader.getAccountNumber(), bcHeader.getSubAccountNumber(), bcHeader.getUniversityFiscalYear())); } private boolean setTestDocumentNumber() { boolean returnValue = false; // use the accounting key to find the document number associated with this funding HashMap<String,Object> fieldValues = new HashMap<String,Object>(4); fieldValues.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR,universityFiscalYear); fieldValues.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE,chartOfAccountsCode); fieldValues.put(KFSPropertyConstants.ACCOUNT_NUMBER,accountNumber); fieldValues.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER,subAccountNumber); Collection<BudgetConstructionHeader> bcDocuments = SpringContext.getBean(BusinessObjectService.class).findMatching(BudgetConstructionHeader.class,fieldValues); // here there should only be one row Iterator<BudgetConstructionHeader> bcHeaderRows = bcDocuments.iterator(); while (bcHeaderRows.hasNext()) { BudgetConstructionHeader bcHeaderRow = bcHeaderRows.next(); fdocNumber = bcHeaderRow.getDocumentNumber(); returnValue = true; // save the header that we've chosen bcHeader = bcHeaderRow; while (bcHeaderRows.hasNext()) { bcHeaderRows.next(); } } return returnValue; } private Integer setTestFiscalYear() { Integer fiscalYear = new Integer(0); // find a fiscal year for which there is active budget construction data. HashMap<String,Object> fieldValues = new HashMap<String,Object>(2); fieldValues.put(KFSPropertyConstants.FINANCIAL_SYSTEM_FUNCTION_CONTROL_CODE,BudgetConstructionConstants.BUDGET_CONSTRUCTION_ACTIVE); fieldValues.put(KFSPropertyConstants.FINANCIAL_SYSTEM_FUNCTION_ACTIVE_INDICATOR,Boolean.TRUE); Collection<FiscalYearFunctionControl> returnedYears = SpringContext.getBean(BusinessObjectService.class).findMatchingOrderBy(FiscalYearFunctionControl.class,fieldValues,"universityFiscalYear",false); // there should be only one, but who knows with test data involved--we'll take the fiscal year from the first one Iterator<FiscalYearFunctionControl> activeYears = returnedYears.iterator(); if (activeYears.hasNext()) { fiscalYear = activeYears.next().getUniversityFiscalYear(); // just run the iterator out, to be tidy while (activeYears.hasNext()) { activeYears.next(); } } return fiscalYear; } private boolean setTestFunding() { boolean returnValue = false; // all we need is a single funding line (not deleted, not vacant) for a real person // but we'll apparently have to get them all and just take the first one HashMap<String,Object> fieldValues = new HashMap<String,Object>(2); // fieldValues.put(BCPropertyConstants.APPOINTMENT_FUNDING_DELETE_INDICATOR,new Boolean(false)); fieldValues.put(KFSPropertyConstants.ACTIVE,new Boolean(true)); fieldValues.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR,universityFiscalYear); // get the complete set of rows and look for the first one that does not have a vacant EMPLID Collection<PendingBudgetConstructionAppointmentFunding> resultSet = SpringContext.getBean(BusinessObjectService.class).findMatching(PendingBudgetConstructionAppointmentFunding.class,fieldValues); Iterator<PendingBudgetConstructionAppointmentFunding> fundingRows = resultSet.iterator(); while (fundingRows.hasNext()) { PendingBudgetConstructionAppointmentFunding fundingRow = fundingRows.next(); if (!fundingRow.getEmplid().equals(BCConstants.VACANT_EMPLID)) { returnValue = true; // set all the test funding values from this row chartOfAccountsCode = fundingRow.getChartOfAccountsCode(); accountNumber = fundingRow.getAccountNumber(); subAccountNumber = fundingRow.getSubAccountNumber(); financialObjectCode = fundingRow.getFinancialObjectCode(); financialSubObjectCode = fundingRow.getFinancialSubObjectCode(); emplid = fundingRow.getEmplid(); positionNumber = fundingRow.getPositionNumber(); // save the row we've selected bcAFunding = fundingRow; // run out the iterator while (fundingRows.hasNext()) { fundingRows.next(); } } } return returnValue; } @ConfigureContext(shouldCommitTransactions = true) private void clearTestRowLocks() { // clear all the locks on the test rows used in this TestCase // make sure there are no locks or transaction locks in our test header lockService.unlockAccount(bcHeader); lockService.unlockTransaction(chartOfAccountsCode, accountNumber, subAccountNumber, universityFiscalYear); // make sure the position we intend to use is unlocked lockService.unlockPosition(positionNumber, universityFiscalYear); // make sure funding locks we intend to use aren't there lockService.unlockFunding(chartOfAccountsCode, accountNumber, subAccountNumber, universityFiscalYear, pUIdOne); lockService.unlockFunding(chartOfAccountsCode, accountNumber, subAccountNumber, universityFiscalYear, pUIdTwo); } }