package com.constellio.model.services.records; import static com.constellio.sdk.tests.TestUtils.asList; import static com.constellio.sdk.tests.TestUtils.asMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isNull; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import com.constellio.app.services.collections.CollectionsManager; import com.constellio.data.dao.dto.records.OptimisticLockingResolution; import com.constellio.data.dao.dto.records.RecordDTO; import com.constellio.data.dao.dto.records.RecordDeltaDTO; import com.constellio.data.dao.dto.records.RecordsFlushing; import com.constellio.data.dao.dto.records.TransactionDTO; import com.constellio.data.dao.dto.records.TransactionResponseDTO; import com.constellio.data.dao.services.DataStoreTypesFactory; import com.constellio.data.dao.services.bigVault.RecordDaoException; import com.constellio.data.dao.services.bigVault.RecordDaoException.NoSuchRecordWithId; import com.constellio.data.dao.services.bigVault.RecordDaoException.OptimisticLocking; import com.constellio.data.dao.services.bigVault.RecordDaoRuntimeException.RecordDaoRuntimeException_RecordsFlushingFailed; import com.constellio.data.dao.services.idGenerator.UniqueIdGenerator; import com.constellio.data.dao.services.records.RecordDao; import com.constellio.data.utils.Factory; import com.constellio.model.entities.batchprocess.BatchProcess; import com.constellio.model.entities.records.Record; import com.constellio.model.entities.records.RecordUpdateOptions; import com.constellio.model.entities.records.Transaction; import com.constellio.model.entities.records.TransactionRecordsReindexation; import com.constellio.model.entities.records.wrappers.User; import com.constellio.model.entities.schemas.Metadata; import com.constellio.model.entities.schemas.MetadataSchema; import com.constellio.model.entities.schemas.MetadataSchemaType; import com.constellio.model.entities.schemas.MetadataSchemaTypes; import com.constellio.model.entities.schemas.ModificationImpact; import com.constellio.model.entities.schemas.Schemas; import com.constellio.model.services.batch.manager.BatchProcessesManager; import com.constellio.model.services.collections.CollectionsListManager; import com.constellio.model.services.contents.ContentManager; import com.constellio.model.services.contents.ContentModifications; import com.constellio.model.services.encrypt.EncryptionServices; import com.constellio.model.services.extensions.ModelLayerExtensions; import com.constellio.model.services.factories.ModelLayerFactory; import com.constellio.model.services.logging.LoggingServices; import com.constellio.model.services.records.RecordServicesRuntimeException.RecordServicesRuntimeException_RecordsFlushingFailed; import com.constellio.model.services.records.RecordServicesRuntimeException.UnresolvableOptimsiticLockingCausingInfiniteLoops; import com.constellio.model.services.records.RecordServicesRuntimeException.UserCannotReadDocument; import com.constellio.model.services.records.cache.RecordsCaches; import com.constellio.model.services.records.extractions.RecordPopulateServices; import com.constellio.model.services.schemas.MetadataList; import com.constellio.model.services.schemas.MetadataSchemasManager; import com.constellio.model.services.schemas.ModificationImpactCalculator; import com.constellio.model.services.search.SearchServices; import com.constellio.model.services.search.query.logical.LogicalSearchQuery; import com.constellio.model.services.search.query.logical.LogicalSearchQueryOperators; import com.constellio.model.services.search.query.logical.condition.LogicalSearchCondition; import com.constellio.model.services.search.query.logical.condition.SolrQueryBuilderParams; import com.constellio.model.services.security.AuthorizationsServices; import com.constellio.model.services.taxonomies.TaxonomiesManager; import com.constellio.sdk.tests.ConstellioTest; import com.constellio.sdk.tests.TestRecord; import com.constellio.sdk.tests.TestUtils; import com.constellio.sdk.tests.schemas.FakeDataStoreTypeFactory; import com.constellio.sdk.tests.schemas.TestsSchemasSetup; import com.constellio.sdk.tests.schemas.TestsSchemasSetup.ZeSchemaMetadatas; public class RecordServicesTest extends ConstellioTest { private final Long theDocumentCount = aLong(); long theNewVersion = 9L; Map<String, Object> dtoValues = asMap("schema_string", (Object) "schematype_default", "collection_s", "zeCollection"); RecordDTO firstSearchResult = new RecordDTO("1", 1, null, dtoValues); RecordDTO secondSearchResult = new RecordDTO("2", 1, null, dtoValues); List<RecordDTO> theSearchResults = Arrays.asList(firstSearchResult, secondSearchResult); RecordDTO recordDTO = new RecordDTO("3", 1, null, dtoValues); @Mock RecordDeltaDTO deltaDTO; DataStoreTypesFactory typesFactory = new FakeDataStoreTypeFactory(); @Mock MetadataSchemasManager schemaManager; @Mock RecordDao recordDao; @Mock RecordDao eventsDao; @Mock RecordDao notificationsDao; @Mock RecordsCaches recordsCaches; @Mock RecordValidationServices validationServices; @Mock RecordAutomaticMetadataServices automaticMetadataServices; @Mock ContentManager contentManager; @Mock RecordModificationImpactHandler recordModificationImpactHandler; @Mock CollectionsListManager collectionsListManager; @Mock RecordPopulateServices recordPopulateServices; @Mock Factory<EncryptionServices> encryptionServiceFactory; @Mock AuthorizationsServices authorizationServices; ModelLayerExtensions extensions = new ModelLayerExtensions(); long firstVersion = anInteger(); long secondVersion = anInteger(); TestRecord record, otherRecord, savedRecord, otherSavedRecord, recordWithATitleAndStringMetadataValue, anotherRecordWithATitleAndStringMetadataValue; String theFreeTextSearch = aString(); String theId = aString(); String anotherSavedDocumentId = aString(); TestsSchemasSetup schemas; ZeSchemaMetadatas zeSchema; RecordServicesImpl recordServices; String theTitle = aString(); String theStringMetadata = aString(); String anotherTitle = aString(); String anotherStringMetadata = aString(); @Mock Metadata firstReindexedMetadata, secondReindexedMetadata; TransactionRecordsReindexation reindexedMetadata; long firstUpdatedRecordVersion = aLong(); long secondUpdatedRecordVersion = aLong(); long firstAddedRecordVersion = aLong(); long secondAddedRecordVersion = aLong(); String firstUpdatedRecordId = "firstUpdatedRecordId"; String secondUpdatedRecordId = "secondUpdatedRecordId"; String firstAddedRecordId = "firstAddedRecordId"; String secondAddedRecordId = "secondAddedRecordId"; @Mock ModificationImpact aModificationImpact; @Mock ModificationImpact anotherModificationImpact; @Mock BatchProcessesManager batchProcessesManager; @Mock SearchServices searchServices; @Mock ModelLayerFactory modelFactory; @Mock Metadata firstMetadataToReindex; @Mock Metadata secondMetadataToReindex; @Mock Metadata thirdMetadataToReindex; @Mock LogicalSearchCondition firstSearchCondition; @Mock LogicalSearchCondition secondSearchCondition; @Mock RecordImpl firstRecordConditionRecord1; @Mock RecordImpl firstRecordConditionRecord2; @Mock RecordImpl secondRecordConditionRecord1; @Mock RecordImpl secondRecordConditionRecord2; @Mock TransactionRecordsReindexation alreadyReindexedMetadata; @Mock OptimisticLocking optimisticLockingException; @Mock TransactionResponseDTO transactionResponseDTO; String firstRecordId = aString(); String secondRecordId = aString(); String thirdRecordId = aString(); long firstRecordVersion = aLong(); long secondRecordVersion = aLong(); long thirdRecordVersion = aLong(); @Mock RecordImpl firstRecord; @Mock RecordImpl secondRecord; @Mock RecordImpl thirdRecord; @Mock RecordImpl newFirstRecordVersion; @Mock RecordImpl newSecondRecordVersion; @Mock TaxonomiesManager taxonomiesManager; @Mock LoggingServices loggingServices; @Mock CollectionsManager collectionsManager; MetadataSchemaTypes metadataSchemaTypes; @Mock MetadataSchemaType metadataSchemaType; @Before public void setUp() throws Exception { schemas = new TestsSchemasSetup(); zeSchema = schemas.new ZeSchemaMetadatas(); UniqueIdGenerator uniqueIdGenerator = new UniqueIdGenerator() { int i = 0; @Override public synchronized String next() { return "" + (++i); } }; doReturn(recordPopulateServices).when(modelFactory).newRecordPopulateServices(); when(newFirstRecordVersion.getId()).thenReturn(firstRecordId); when(newSecondRecordVersion.getId()).thenReturn(secondRecordId); when(newFirstRecordVersion.getSchemaCode()).thenReturn(zeSchema.code()); when(newSecondRecordVersion.getSchemaCode()).thenReturn(zeSchema.code()); when(newFirstRecordVersion.getCollection()).thenReturn(zeCollection); when(newSecondRecordVersion.getCollection()).thenReturn(zeCollection); when(firstRecord.getId()).thenReturn(firstRecordId); when(secondRecord.getId()).thenReturn(secondRecordId); when(thirdRecord.getId()).thenReturn(thirdRecordId); when(firstRecord.getCollection()).thenReturn(zeCollection); when(secondRecord.getCollection()).thenReturn(zeCollection); when(thirdRecord.getCollection()).thenReturn(zeCollection); when(firstRecord.getSchemaCode()).thenReturn(zeSchema.code()); when(secondRecord.getSchemaCode()).thenReturn(zeSchema.code()); when(thirdRecord.getSchemaCode()).thenReturn(zeSchema.code()); when(firstRecord.getVersion()).thenReturn(firstRecordVersion); when(secondRecord.getVersion()).thenReturn(secondRecordVersion); when(thirdRecord.getVersion()).thenReturn(thirdRecordVersion); when(modelFactory.getBatchProcessesManager()).thenReturn(batchProcessesManager); when(modelFactory.getMetadataSchemasManager()).thenReturn(schemaManager); when(modelFactory.newSearchServices()).thenReturn(searchServices); when(modelFactory.getTaxonomiesManager()).thenReturn(taxonomiesManager); when(modelFactory.getContentManager()).thenReturn(contentManager); when(modelFactory.newLoggingServices()).thenReturn(loggingServices); when(modelFactory.getExtensions()).thenReturn(extensions); when(collectionsManager.getCollectionLanguages(zeCollection)).thenReturn(Arrays.asList("fr", "en")); recordServices = spy( (RecordServicesImpl) new RecordServicesImpl(recordDao, eventsDao, notificationsDao, modelFactory, typesFactory, uniqueIdGenerator, recordsCaches)); doNothing().when(recordServices).sleep(anyLong()); doReturn(validationServices).when(recordServices).newRecordValidationServices(any(RecordProvider.class)); doReturn(automaticMetadataServices).when(recordServices).newAutomaticMetadataServices(); define(schemaManager).using(schemas.withATitle().withAStringMetadata()); record = spy(new TestRecord(zeSchema, "record")); otherRecord = spy(new TestRecord(zeSchema, "otherRecord")); savedRecord = spy(new TestRecord(zeSchema, "savedRecord")); theId = savedRecord.getId(); savedRecord.refresh(firstVersion, TestUtils.newRecordDTO("savedRecord", zeSchema)); otherSavedRecord = spy(new TestRecord(zeSchema, "otherSavedRecord")); anotherSavedDocumentId = otherSavedRecord.getId(); otherSavedRecord.refresh(firstVersion, TestUtils.newRecordDTO("otherSavedRecord", zeSchema)); recordWithATitleAndStringMetadataValue = spy(new TestRecord(zeSchema, "recordWithATitleAndStringMetadataValue")); recordWithATitleAndStringMetadataValue.set(zeSchema.title(), theTitle); recordWithATitleAndStringMetadataValue.set(zeSchema.stringMetadata(), theStringMetadata); anotherRecordWithATitleAndStringMetadataValue = spy(new TestRecord(zeSchema, "recordWithATitleAndStringMetadataValue")); anotherRecordWithATitleAndStringMetadataValue.set(zeSchema.title(), anotherTitle); anotherRecordWithATitleAndStringMetadataValue.set(zeSchema.stringMetadata(), anotherStringMetadata); reindexedMetadata = new TransactionRecordsReindexation(new MetadataList(firstReindexedMetadata, secondReindexedMetadata)); when(firstRecordConditionRecord1.getSchemaCode()).thenReturn(schemas.anotherDefaultSchemaCode()); when(firstRecordConditionRecord2.getSchemaCode()).thenReturn(schemas.anotherDefaultSchemaCode()); when(secondRecordConditionRecord1.getSchemaCode()).thenReturn(schemas.anotherDefaultSchemaCode()); when(secondRecordConditionRecord2.getSchemaCode()).thenReturn(schemas.anotherDefaultSchemaCode()); when(firstRecordConditionRecord1.getId()).thenReturn("firstRecordConditionRecord1"); when(firstRecordConditionRecord2.getId()).thenReturn("firstRecordConditionRecord2"); when(secondRecordConditionRecord1.getId()).thenReturn("secondRecordConditionRecord1"); when(secondRecordConditionRecord2.getId()).thenReturn("secondRecordConditionRecord2"); when(firstRecordConditionRecord1.getCollection()).thenReturn("zeCollection"); when(firstRecordConditionRecord2.getCollection()).thenReturn("zeCollection"); when(secondRecordConditionRecord1.getCollection()).thenReturn("zeCollection"); when(secondRecordConditionRecord2.getCollection()).thenReturn("zeCollection"); when(firstRecord.getCollection()).thenReturn("zeCollection"); when(secondRecord.getCollection()).thenReturn("zeCollection"); when(thirdRecord.getCollection()).thenReturn("zeCollection"); metadataSchemaTypes = schemaManager.getSchemaTypes(zeCollection); when(modelFactory.getCollectionsListManager()).thenReturn(collectionsListManager); when(collectionsListManager.getCollectionLanguages(anyString())).thenReturn(asList("fr")); } @Test public void whenGettingDocumentsCountTheReturnDaoDocumentsCount() { when(recordDao.documentsCount()).thenReturn(theDocumentCount); assertThat(recordServices.documentsCount()).isEqualTo(theDocumentCount); } @Test public void givenRecordDTOWhenGetDocumentByIdThenRecordHasRecordDTO() throws Exception { when(recordDao.get(theId)).thenReturn(recordDTO); RecordImpl recordObtained = (RecordImpl) recordServices.getDocumentById(theId); assertThat(recordObtained.getRecordDTO()).isEqualTo(recordDTO); } @SuppressWarnings("unchecked") @Test(expected = RecordServicesRuntimeException.NoSuchRecordWithId.class) public void givenInexistentIdWhenGetDocumentByIdThenThrowException() throws Exception { when(recordDao.get(theId)).thenThrow(RecordDaoException.NoSuchRecordWithId.class); recordServices.getDocumentById(theId); } @SuppressWarnings("unchecked") @Test(expected = UserCannotReadDocument.class) public void givenUnauthorizedAccessWhenGetDocumentByIdThenThrowException() throws Exception { User theUser = mock(User.class, "theUser"); when(recordDao.get(theId)).thenReturn(recordDTO); when(authorizationServices.canRead(eq(theUser), any(Record.class))).thenReturn(false); when(modelFactory.newAuthorizationsServices()).thenReturn(authorizationServices); when(schemaManager.getSchemaTypeOf(any(Record.class))).thenReturn(metadataSchemaType); when(metadataSchemaType.hasSecurity()).thenReturn(true); recordServices.getDocumentById(theId, theUser); } @Test public void givenAuthorizedAccessWhenGetDocumentByIdThenRecordReturned() throws Exception { User theUser = mock(User.class, "theUser"); when(recordDao.get(theId)).thenReturn(recordDTO); when(authorizationServices.canRead(eq(theUser), any(Record.class))).thenReturn(true); when(modelFactory.newAuthorizationsServices()).thenReturn(authorizationServices); when(schemaManager.getSchemaTypeOf(any(Record.class))).thenReturn(metadataSchemaType); when(metadataSchemaType.hasSecurity()).thenReturn(true); assertThat(recordServices.getDocumentById(theId, theUser)).isNotNull(); } @Test public void givenRecordOfSchemaTypeWithoutSecurityWhenGetDocumentByIdThenRecordReturned() throws Exception { User theUser = mock(User.class, "theUser"); when(recordDao.get(theId)).thenReturn(recordDTO); when(authorizationServices.canRead(eq(theUser), any(Record.class))).thenReturn(false); when(modelFactory.newAuthorizationsServices()).thenReturn(authorizationServices); when(schemaManager.getSchemaTypeOf(any(Record.class))).thenReturn(metadataSchemaType); when(metadataSchemaType.hasSecurity()).thenReturn(false); assertThat(recordServices.getDocumentById(theId, theUser)).isNotNull(); } @Test public void whenAddingRecordThenSaveInTransaction() throws Exception { ArgumentCaptor<Transaction> transaction = ArgumentCaptor.forClass(Transaction.class); when(recordDao.get(theId)).thenReturn(recordDTO); doNothing().when(recordServices).execute(any(Transaction.class)); recordServices.add(record); verify(recordServices).execute(transaction.capture()); assertThat(transaction.getValue().getRecords()).containsOnly(record); } @Test public void whenUpdatingRecordThenSaveInTransaction() throws Exception { ArgumentCaptor<Transaction> transaction = ArgumentCaptor.forClass(Transaction.class); when(recordDao.get(theId)).thenReturn(recordDTO); doNothing().when(recordServices).execute(any(Transaction.class)); RecordUpdateOptions options = mock(RecordUpdateOptions.class); record.set(zeSchema.title(), "value"); recordServices.update(record, options); verify(recordServices).execute(transaction.capture()); assertThat(transaction.getValue().getRecords()).containsOnly(record); assertThat(transaction.getValue().getRecordUpdateOptions()).isSameAs(options); } @Test public void whenUpdatingUnsavedRecordThenExecuteInTransactionAnyway() throws Exception { when(recordDao.get(theId)).thenReturn(recordDTO); doNothing().when(recordServices).execute(any(Transaction.class)); RecordUpdateOptions options = mock(RecordUpdateOptions.class); recordServices.update(record, options); verify(recordServices).execute(any(Transaction.class)); verifyZeroInteractions(recordDao); } @Test public void whenUpdatingRecordHandlingImpactsAsyncThenExecuteWithDefaultOptions() throws Exception { List<BatchProcess> batchProcesses = mock(List.class); ArgumentCaptor<Transaction> transaction = ArgumentCaptor.forClass(Transaction.class); when(recordDao.get(theId)).thenReturn(recordDTO); doReturn(batchProcesses).when(recordServices).executeHandlingImpactsAsync(any(Transaction.class)); RecordUpdateOptions options = mock(RecordUpdateOptions.class); List<BatchProcess> returnedBatchProcesses = recordServices.updateAsync(record, options); verify(recordServices).executeHandlingImpactsAsync(transaction.capture()); assertThat(transaction.getValue().getRecords()).containsOnly(record); assertThat(transaction.getValue().getRecordUpdateOptions()).isEqualTo(options); assertThat(returnedBatchProcesses).isEqualTo(batchProcesses); } @Test public void whenUpdatingRecordHandlingImpactsAsyncThenExecuteInAsyncTransaction() throws Exception { List<BatchProcess> batchProcesses = mock(List.class); ArgumentCaptor<Transaction> transaction = ArgumentCaptor.forClass(Transaction.class); when(recordDao.get(theId)).thenReturn(recordDTO); doReturn(batchProcesses).when(recordServices).executeHandlingImpactsAsync(any(Transaction.class)); List<BatchProcess> returnedBatchProcesses = recordServices.updateAsync(record); verify(recordServices).executeHandlingImpactsAsync(transaction.capture()); assertThat(transaction.getValue().getRecords()).containsOnly(record); assertThat(transaction.getValue().getRecordUpdateOptions()).isNotNull(); assertThat(returnedBatchProcesses).isEqualTo(batchProcesses); } @Test public void whenExecutingAsyncThenReturnHandlerBatchProcesses() throws Exception { List<BatchProcess> batchProcesses = mock(List.class); Transaction transaction = mock(Transaction.class); List<Record> someRecords = Arrays.asList(mock(Record.class)); when(transaction.getRecords()).thenReturn(someRecords); AddToBatchProcessImpactHandler handler = mock(AddToBatchProcessImpactHandler.class); doReturn(handler).when(recordServices).addToBatchProcessModificationImpactHandler(); when(handler.getAllCreatedBatchProcesses()).thenReturn(batchProcesses); doNothing().when(recordServices).executeWithImpactHandler(any(Transaction.class), any(AddToBatchProcessImpactHandler.class)); List<BatchProcess> returnedBatchProcesses = recordServices.executeHandlingImpactsAsync(transaction); verify(recordServices).executeWithImpactHandler(transaction, handler); assertThat(returnedBatchProcesses).isEqualTo(batchProcesses); } @Test public void whenCreatingTransactionDTOThenAddRecordsAndUpdateRecords() throws Exception { record.set(zeSchema.stringMetadata(), "recordString"); record.set(zeSchema.title(), "recordTitle"); otherRecord.set(zeSchema.stringMetadata(), "otherRecordString"); otherRecord.set(zeSchema.title(), "otherRecordTitle"); savedRecord.set(zeSchema.stringMetadata(), "savedRecordString"); savedRecord.set(zeSchema.title(), "savedRecordTitle"); otherSavedRecord.set(zeSchema.stringMetadata(), "otherSavedRecordString"); otherSavedRecord.set(zeSchema.title(), "otherSavedRecordTitle"); RecordsFlushing recordsFlushing = mock(RecordsFlushing.class); Transaction transaction = new Transaction(); transaction.addUpdate(record); transaction.addUpdate(otherRecord); transaction.addUpdate(savedRecord); transaction.addUpdate(otherSavedRecord); TransactionDTO transactionDTO = recordServices.createTransactionDTO(transaction, transaction.getModifiedRecords()); assertThat(transactionDTO.getNewRecords()).hasSize(2); assertThat(transactionDTO.getModifiedRecords()).hasSize(2); RecordDTO firstRecordDTO = transactionDTO.getNewRecords().get(0); RecordDTO secondRecordDTO = transactionDTO.getNewRecords().get(1); RecordDeltaDTO firstDeltaRecordDTO = transactionDTO.getModifiedRecords().get(0); RecordDeltaDTO secondDeltaRecordDTO = transactionDTO.getModifiedRecords().get(1); assertThat(firstRecordDTO.getFields()).containsEntry(zeSchema.stringMetadata().getDataStoreCode(), "recordString"); assertThat(firstRecordDTO.getFields()).containsEntry(zeSchema.title().getDataStoreCode(), "recordTitle"); assertThat(secondRecordDTO.getFields()).containsEntry(zeSchema.stringMetadata().getDataStoreCode(), "otherRecordString"); assertThat(secondRecordDTO.getFields()).containsEntry(zeSchema.title().getDataStoreCode(), "otherRecordTitle"); assertThat(firstDeltaRecordDTO.getModifiedFields()).containsEntry(zeSchema.stringMetadata().getDataStoreCode(), "savedRecordString"); assertThat(firstDeltaRecordDTO.getModifiedFields()) .containsEntry(zeSchema.title().getDataStoreCode(), "savedRecordTitle"); assertThat(secondDeltaRecordDTO.getModifiedFields()).containsEntry(zeSchema.stringMetadata().getDataStoreCode(), "otherSavedRecordString"); assertThat(secondDeltaRecordDTO.getModifiedFields()).containsEntry(zeSchema.title().getDataStoreCode(), "otherSavedRecordTitle"); } @Test public void whenCreatingTransactionDTOThenDotNotUpdateRecordsWithoutModifications() throws Exception { record.set(zeSchema.stringMetadata(), "recordString"); record.set(zeSchema.title(), "recordTitle"); Transaction transaction = new Transaction(); transaction.addUpdate(record); transaction.addUpdate(savedRecord); transaction.addUpdate(otherSavedRecord); TransactionDTO transactionDTO = recordServices.createTransactionDTO(transaction, transaction.getModifiedRecords()); assertThat(transactionDTO.getNewRecords()).hasSize(1); assertThat(transactionDTO.getModifiedRecords()).hasSize(0); RecordDTO firstRecordDTO = transactionDTO.getNewRecords().get(0); assertThat(firstRecordDTO.getFields()).containsEntry(zeSchema.stringMetadata().getDataStoreCode(), "recordString"); assertThat(firstRecordDTO.getFields()).containsEntry(zeSchema.title().getDataStoreCode(), "recordTitle"); } @SuppressWarnings("unchecked") @Test public void whenExecutingTransactionThenPrepareRecordsAndAddThemInATransaction() throws Exception { RecordsFlushing recordsFlushing = mock(RecordsFlushing.class); TransactionDTO transactionDTO = mock(TransactionDTO.class); Transaction transaction = new Transaction(); transaction.addUpdate(record); transaction.addUpdate(otherRecord); transaction.addUpdate(savedRecord); transaction.addUpdate(otherSavedRecord); transaction.setRecordFlushing(recordsFlushing); doReturn(transactionDTO).when(recordServices).createTransactionDTO(eq(transaction), anyList()); doReturn(transactionResponseDTO).when(recordDao).execute(transactionDTO); doNothing().when(recordServices).refreshRecordsAndCaches(eq(zeCollection), anyList(), any(TransactionResponseDTO.class), any(MetadataSchemaTypes.class)); recordServices.execute(transaction); verify(recordDao).execute(transactionDTO); verify(recordServices).prepareRecords(transaction); } @SuppressWarnings("unchecked") @Test public void givenOptimisticLockingExceptionWhenExecutingTransactionThenHandleIt() throws Exception { RecordsFlushing recordsFlushing = mock(RecordsFlushing.class); TransactionDTO transactionDTO = mock(TransactionDTO.class); Transaction transaction = new Transaction(); transaction.addUpdate(record); transaction.addUpdate(otherRecord); transaction.addUpdate(savedRecord); transaction.addUpdate(otherSavedRecord); transaction.setRecordFlushing(recordsFlushing); doReturn(transactionDTO).when(recordServices) .createTransactionDTO(eq(transaction), anyList()); doNothing().when(recordServices).refreshRecordsAndCaches(eq(zeCollection), anyList(), any(TransactionResponseDTO.class), any(MetadataSchemaTypes.class)); doNothing().when(recordServices).handleOptimisticLocking(any(TransactionDTO.class), any(Transaction.class), any(RecordModificationImpactHandler.class), any(OptimisticLocking.class), anyInt()); RecordDaoException.OptimisticLocking exception = mock(RecordDaoException.OptimisticLocking.class); doThrow(exception).when(recordDao).execute(transactionDTO); recordServices.execute(transaction); verify(recordServices) .handleOptimisticLocking(any(TransactionDTO.class), eq(transaction), isNull(RecordModificationImpactHandler.class), eq(exception), anyInt()); } @Test public void whenHandlingOptimisticLockingWithExceptionThenThrowException() throws Exception { Transaction transaction = new Transaction(); transaction.setOptimisticLockingResolution(OptimisticLockingResolution.EXCEPTION); try { recordServices .handleOptimisticLocking(mock(TransactionDTO.class), transaction, recordModificationImpactHandler, optimisticLockingException, 0); fail("Exception expected"); } catch (RecordServicesException.OptimisticLocking e) { // OK } verify(recordServices) .handleOptimisticLocking(any(TransactionDTO.class), eq(transaction), eq(recordModificationImpactHandler), eq(optimisticLockingException), anyInt()); verifyZeroInteractions(recordDao); } @Test public void whenHandlingOptimisticLockingKeepingOlderThenDoNothing() throws Exception { Transaction transaction = new Transaction(); transaction.setOptimisticLockingResolution(OptimisticLockingResolution.KEEP_OLDER); transaction.add(record); recordServices.handleOptimisticLocking(mock(TransactionDTO.class), transaction, recordModificationImpactHandler, optimisticLockingException, 0); verify(recordServices) .handleOptimisticLocking(any(TransactionDTO.class), eq(transaction), eq(recordModificationImpactHandler), eq(optimisticLockingException), anyInt()); verifyZeroInteractions(recordDao); } @Test public void givenOptimisticLockingInAsyncTransactionWhenHandlingOptimisticLockingWithMergeThenMergeAndExecuteNewTransactionAsync() throws Exception { Transaction transaction = new Transaction(); transaction.setOptimisticLockingResolution(OptimisticLockingResolution.TRY_MERGE); transaction.add(record); doNothing().when(recordServices).refreshRecordsAndCaches(anyString(), anyList(), any(TransactionResponseDTO.class), any(MetadataSchemaTypes.class)); doNothing().when(recordServices).mergeRecords(eq(transaction), anyString()); doNothing().when(recordServices).executeWithImpactHandler(any(Transaction.class), any(RecordModificationImpactHandler.class)); recordServices.handleOptimisticLocking(mock(TransactionDTO.class), transaction, recordModificationImpactHandler, optimisticLockingException, 3); InOrder inOrder = inOrder(recordServices); inOrder.verify(recordServices).mergeRecords(eq(transaction), anyString()); inOrder.verify(recordServices).executeWithImpactHandler(transaction, recordModificationImpactHandler, 4); } @Test public void givenOptimisticLockingInTransactionWhenHandlingOptimisticLockingWithMergeThenMergeAndExecuteNewTransaction() throws Exception { Transaction transaction = new Transaction(); transaction.setOptimisticLockingResolution(OptimisticLockingResolution.TRY_MERGE); transaction.add(record); doNothing().when(recordServices).refreshRecordsAndCaches(anyString(), anyList(), any(TransactionResponseDTO.class), any(MetadataSchemaTypes.class)); doNothing().when(recordServices).mergeRecords(any(Transaction.class), anyString()); doNothing().when(recordServices).execute(any(Transaction.class)); recordServices .handleOptimisticLocking(mock(TransactionDTO.class), transaction, null, optimisticLockingException, 2); InOrder inOrder = inOrder(recordServices); inOrder.verify(recordServices).mergeRecords(eq(transaction), anyString()); inOrder.verify(recordServices).execute(transaction, 3); } @Test public void whenMergingThenGetListOfModifiedDocumentsAndMergeEachDocument() throws Exception { ArgumentCaptor<LogicalSearchQuery> query = ArgumentCaptor.forClass(LogicalSearchQuery.class); when(firstRecord.isSaved()).thenReturn(true); when(secondRecord.isSaved()).thenReturn(true); when(thirdRecord.isSaved()).thenReturn(true); Transaction transaction = new Transaction(); transaction.addUpdate(asList((Record) firstRecord, secondRecord, thirdRecord)); List<Record> modifiedRecords = Arrays.asList((Record) newFirstRecordVersion, newSecondRecordVersion); when(searchServices.search(query.capture())).thenReturn(modifiedRecords); recordServices.mergeRecords(transaction, "zeId"); verify(firstRecord).merge(eq(newFirstRecordVersion), any(MetadataSchema.class)); verify(secondRecord).merge(eq(newSecondRecordVersion), any(MetadataSchema.class)); verify(thirdRecord, never()).merge(any(RecordImpl.class), any(MetadataSchema.class)); LogicalSearchCondition condition = query.getValue().getCondition(); LogicalSearchCondition firstRecordCondition = LogicalSearchQueryOperators.where(Schemas.IDENTIFIER).is(firstRecordId) .andWhere(Schemas.VERSION).isNotEqual(firstRecordVersion); LogicalSearchCondition secondRecordCondition = LogicalSearchQueryOperators.where(Schemas.IDENTIFIER).is(secondRecordId) .andWhere(Schemas.VERSION).isNotEqual(secondRecordVersion); LogicalSearchCondition thirdRecordCondition = LogicalSearchQueryOperators.where(Schemas.IDENTIFIER).is(thirdRecordId) .andWhere(Schemas.VERSION).isNotEqual(thirdRecordVersion); SolrQueryBuilderParams params = new SolrQueryBuilderParams(false, null); assertThat(condition.getSolrQuery(params)).isEqualTo( LogicalSearchQueryOperators.fromAllSchemasIn(condition.getCollection()) .whereAnyCondition(Arrays.asList(firstRecordCondition, secondRecordCondition, thirdRecordCondition)) .getSolrQuery(params)); } @Test(expected = RecordServicesException.UnresolvableOptimisticLockingConflict.class) public void givenOneDocumentCannotBeMergedWhenMergingThenException() throws Exception { when(firstRecord.isSaved()).thenReturn(true); when(secondRecord.isSaved()).thenReturn(true); when(thirdRecord.isSaved()).thenReturn(true); List<Record> modifiedRecords = Arrays.asList((Record) newFirstRecordVersion, newSecondRecordVersion); Transaction transaction = new Transaction(); transaction.addUpdate(asList((Record) firstRecord, secondRecord, thirdRecord)); doThrow(RecordServicesException.UnresolvableOptimisticLockingConflict.class).when(secondRecord).merge( eq(newSecondRecordVersion), any(MetadataSchema.class)); when(searchServices.search(any(LogicalSearchQuery.class))).thenReturn(modifiedRecords); recordServices.mergeRecords(transaction, "zeId"); } @Test public void whenRefreshRecordsThenRefreshRecords() throws Exception { when(transactionResponseDTO.getNewDocumentVersion(firstAddedRecordId)).thenReturn(firstAddedRecordVersion); when(transactionResponseDTO.getNewDocumentVersion(secondAddedRecordId)).thenReturn(secondAddedRecordVersion); when(transactionResponseDTO.getNewDocumentVersion(firstUpdatedRecordId)).thenReturn(firstUpdatedRecordVersion); when(transactionResponseDTO.getNewDocumentVersion(secondUpdatedRecordId)).thenReturn(secondUpdatedRecordVersion); RecordImpl firstUpdatedRecord = spy(new TestRecord(zeSchema, firstUpdatedRecordId)); RecordImpl firstAddedRecord = spy(new TestRecord(zeSchema, firstAddedRecordId)); RecordImpl secondAddedRecord = spy(new TestRecord(zeSchema, secondAddedRecordId)); RecordImpl secondUpdatedRecord = spy(new TestRecord(zeSchema, secondUpdatedRecordId)); List<Record> records = asList((Record) firstUpdatedRecord, firstAddedRecord, secondAddedRecord, secondUpdatedRecord); recordServices.refreshRecordsAndCaches(zeCollection, records, transactionResponseDTO, metadataSchemaTypes); verify(firstAddedRecord).markAsSaved(firstAddedRecordVersion, zeSchema.instance()); verify(secondAddedRecord).markAsSaved(secondAddedRecordVersion, zeSchema.instance()); verify(firstUpdatedRecord).markAsSaved(firstUpdatedRecordVersion, zeSchema.instance()); verify(secondUpdatedRecord).markAsSaved(secondUpdatedRecordVersion, zeSchema.instance()); } private Record recordWithIdAndDTO(String id, RecordDTO dto) { RecordImpl record = mock(RecordImpl.class, id); when(record.getId()).thenReturn(id); when(record.getRecordDTO()).thenReturn(dto); return record; } @Test public void givenRecordNotDirtyUpdatedInTransactionThenNotSaved() throws Exception { RecordImpl zeRecord = spy(new TestRecord(zeSchema)); when(zeRecord.getId()).thenReturn("anId"); when(zeRecord.isDirty()).thenReturn(false); Transaction transaction = new Transaction(); transaction.update(zeRecord); verifyZeroInteractions(recordDao); } @Test public void givenNoModificationImpactHandlerDefinedWhenExecutingTransactionUpdatingRecordWithModificationImpactThenException() throws Exception { AddToBatchProcessImpactHandler defaultHandler = mock(AddToBatchProcessImpactHandler.class); RecordImpl zeRecord = spy(new TestRecord(zeSchema)); when(zeRecord.getId()).thenReturn("anId"); when(zeRecord.isDirty()).thenReturn(true); doNothing().when(recordServices).refreshRecordsAndCaches(eq(zeCollection), anyList(), any(TransactionResponseDTO.class), any(MetadataSchemaTypes.class)); Transaction transaction = new Transaction(); transaction.update(zeRecord); transaction.getRecordUpdateOptions().setForcedReindexationOfMetadatas(alreadyReindexedMetadata); doReturn(asList(aModificationImpact, anotherModificationImpact)).when(recordServices).calculateImpactOfModification( transaction, taxonomiesManager, searchServices, metadataSchemaTypes, true); doReturn(defaultHandler).when(recordServices).addToBatchProcessModificationImpactHandler(); recordServices.executeHandlingImpactsAsync(transaction); verify(defaultHandler).prepareToHandle(aModificationImpact); verify(defaultHandler).prepareToHandle(anotherModificationImpact); verify(defaultHandler).handle(); } @Test public void whenExecutingWithImpactHandlerATransactionWithModificationImpactThenImpactHandledAfterTransaction() throws Exception { RecordsFlushing recordsFlushing = mock(RecordsFlushing.class); RecordImpl zeRecord = spy(new TestRecord(zeSchema)); when(zeRecord.getId()).thenReturn("anId"); when(zeRecord.isDirty()).thenReturn(true); Transaction transaction = new Transaction(); transaction.getRecordUpdateOptions().setForcedReindexationOfMetadatas(alreadyReindexedMetadata); transaction.update(zeRecord); transaction.setRecordFlushing(recordsFlushing); doNothing().when(recordServices).refreshRecordsAndCaches(eq(zeCollection), anyList(), any(TransactionResponseDTO.class), any(MetadataSchemaTypes.class)); doReturn(asList(aModificationImpact, anotherModificationImpact)).when(recordServices).calculateImpactOfModification( transaction, taxonomiesManager, searchServices, metadataSchemaTypes, true); RecordModificationImpactHandler handler = mock(RecordModificationImpactHandler.class); TransactionDTO transactionDTO = mock(TransactionDTO.class); doReturn(transactionDTO).when(recordServices) .createTransactionDTO(eq(transaction), anyList()); recordServices.executeWithImpactHandler(transaction, handler); InOrder inOrder = inOrder(recordDao, handler); inOrder.verify(handler).prepareToHandle(aModificationImpact); inOrder.verify(handler).prepareToHandle(anotherModificationImpact); inOrder.verify(recordDao).execute(transactionDTO); inOrder.verify(handler).handle(); } @Test public void whenExecutingTransactionAndUpdatedRecordHasModificationImpactThenExecuteWithImpactedRecordsInNewTransaction() throws RecordServicesException { ArgumentCaptor<Transaction> nestedTransaction = ArgumentCaptor.forClass(Transaction.class); when(aModificationImpact.getMetadataToReindex()).thenReturn(asList(firstReindexedMetadata, secondReindexedMetadata)); when(aModificationImpact.getLogicalSearchCondition()).thenReturn(firstSearchCondition); when(anotherModificationImpact.getMetadataToReindex()).thenReturn(asList(firstReindexedMetadata, thirdMetadataToReindex)); when(anotherModificationImpact.getLogicalSearchCondition()).thenReturn(secondSearchCondition); when(searchServices.search(new LogicalSearchQuery(firstSearchCondition))).thenReturn( asList((Record) firstRecordConditionRecord1, firstRecordConditionRecord2)); when(searchServices.search(new LogicalSearchQuery(secondSearchCondition))).thenReturn( asList((Record) secondRecordConditionRecord1, secondRecordConditionRecord2)); RecordImpl zeRecord = spy(new TestRecord(zeSchema)); when(zeRecord.getId()).thenReturn("anId"); when(zeRecord.isDirty()).thenReturn(true); Transaction transaction = spy(new Transaction()); transaction.update(zeRecord); doReturn(asList(aModificationImpact, anotherModificationImpact)).when(recordServices).getModificationImpacts(transaction, false); doNothing().when(recordServices).refreshRecordsAndCaches(eq(zeCollection), anyList(), any(TransactionResponseDTO.class), any(MetadataSchemaTypes.class)); doNothing().when(recordServices).prepareRecords(any(Transaction.class)); doNothing().when(recordServices).saveContentsAndRecords(any(Transaction.class), any(RecordModificationImpactHandler.class), anyInt()); recordServices.execute(transaction); InOrder inOrder = inOrder(recordServices, transaction); inOrder.verify(recordServices).execute(transaction); inOrder.verify(transaction).sortRecords(schemaManager.getSchemaTypes(zeCollection)); inOrder.verify(recordServices).execute(nestedTransaction.capture()); inOrder.verify(recordServices).saveContentsAndRecords(eq(nestedTransaction.getValue()), isNull(RecordModificationImpactHandler.class), anyInt()); verify(recordServices, never()).saveContentsAndRecords(eq(transaction), isNull(RecordModificationImpactHandler.class), anyInt()); assertThat(nestedTransaction.getValue().getRecords()).containsExactly(firstRecordConditionRecord1, firstRecordConditionRecord2, secondRecordConditionRecord1, secondRecordConditionRecord2, zeRecord); } @Test public void whenRefreshingRecordThenObtainRecordDTOAndRefreshRecord() throws Exception { Record firstRecord = mock(RecordImpl.class); String firstRecordId = aString(); when(firstRecord.getId()).thenReturn(firstRecordId); when(firstRecord.isSaved()).thenReturn(true); Record deletedRecord = mock(RecordImpl.class); String deletedRecordId = aString(); when(deletedRecord.getId()).thenReturn(deletedRecordId); when(deletedRecord.isSaved()).thenReturn(true); Record newRecord = mock(RecordImpl.class); RecordDTO currentFirstRecordDTO = mock(RecordDTO.class); long currentFirstRecordDTOVersion = aLong(); when(currentFirstRecordDTO.getVersion()).thenReturn(currentFirstRecordDTOVersion); when(recordDao.get(firstRecordId)).thenReturn(currentFirstRecordDTO); when(recordDao.get(deletedRecordId)).thenThrow(NoSuchRecordWithId.class); recordServices.refresh(asList(firstRecord, deletedRecord, newRecord)); verify((RecordImpl) firstRecord).refresh(currentFirstRecordDTOVersion, currentFirstRecordDTO); verify((RecordImpl) firstRecord, never()).markAsDisconnected(); verify((RecordImpl) deletedRecord, never()).refresh(currentFirstRecordDTOVersion, currentFirstRecordDTO); verify((RecordImpl) deletedRecord).markAsDisconnected(); verify((RecordImpl) newRecord, never()).refresh(anyLong(), any(RecordDTO.class)); verify((RecordImpl) newRecord, never()).markAsDisconnected(); } @Test public void whenCalculatingModificationImpactThenCallModificationImpactCalculator() throws Exception { List<String> transactionIds = new ArrayList<>(); Record zeRecord = mock(Record.class); List<ModificationImpact> zeModifications = mock(List.class); ModificationImpactCalculator impactCalculator = mock(ModificationImpactCalculator.class); List<Metadata> alreadyReindexedMetadata = new ArrayList<>(); Transaction transaction = new Transaction(zeRecord); when(impactCalculator.findTransactionImpact(transaction, true)).thenReturn(zeModifications); doReturn(impactCalculator).when(recordServices).newModificationImpactCalculator(taxonomiesManager, metadataSchemaTypes, searchServices); assertThat(recordServices.calculateImpactOfModification( transaction, taxonomiesManager, searchServices, metadataSchemaTypes, true)).isEqualTo(zeModifications); } @Test public void givenUnresolvableOptimistickLockingWhenExecutingATransactionThenNoInfiniteLoop() throws Exception { RecordImpl zeRecord = spy(new TestRecord(zeSchema)); when(recordDao.get("anId")).thenThrow(RecordDaoException.NoSuchRecordWithId.class); when(zeRecord.getId()).thenReturn("anId"); when(zeRecord.isDirty()).thenReturn(true); Transaction transaction = new Transaction(zeRecord); doNothing().when(recordServices).mergeRecords(any(Transaction.class), anyString()); doThrow(RecordDaoException.OptimisticLocking.class).when(recordDao).execute(any(TransactionDTO.class)); try { recordServices.execute(transaction); fail("Exception expected"); } catch (UnresolvableOptimsiticLockingCausingInfiniteLoops e) { e.printStackTrace(); } } @Test public void givenUnresolvableOptimistickLockingWhenExecutingATransactionHandlingImpactsAsyncThenNoInfiniteLoop() throws Exception { RecordImpl zeRecord = spy(new TestRecord(zeSchema)); when(recordDao.get("anId")).thenThrow(RecordDaoException.NoSuchRecordWithId.class); when(zeRecord.getId()).thenReturn("anId"); when(zeRecord.isDirty()).thenReturn(true); Transaction transaction = new Transaction(zeRecord); doNothing().when(recordServices).mergeRecords(any(Transaction.class), anyString()); doThrow(RecordDaoException.OptimisticLocking.class).when(recordDao).execute(any(TransactionDTO.class)); try { recordServices.executeHandlingImpactsAsync(transaction); fail("Exception expected"); } catch (UnresolvableOptimsiticLockingCausingInfiniteLoops e) { e.printStackTrace(); } } @Test public void givenRecordServicesExceptionCausedByDTOWhenSavingContentAndRecordsThenDeleteAllNewContents() throws Exception { String firstHash = "firstHash"; String secondHash = "secondHash"; List<String> newContents = Arrays.asList(firstHash, secondHash); RecordServicesException zeException = new RecordServicesException("test"); Transaction transaction = mock(Transaction.class); when(transaction.getCollection()).thenReturn(zeCollection); doReturn(new ContentModifications(new ArrayList<String>(), newContents)).when(recordServices) .findContentsModificationsIn(metadataSchemaTypes, transaction); doThrow(zeException).when(recordServices).saveTransactionDTO(eq(transaction), eq(recordModificationImpactHandler), anyInt()); try { recordServices.saveContentsAndRecords(transaction, recordModificationImpactHandler, 0); fail("Exception expected"); } catch (Exception e) { assertThat(e).isEqualTo(zeException); verify(contentManager).silentlyMarkForDeletionIfNotReferenced(firstHash); verify(contentManager).silentlyMarkForDeletionIfNotReferenced(secondHash); } } @Test public void givenRecordServicesRuntimeExceptionCausedByDTOWhenSavingContentAndRecordsThenDeleteAllNewContents() throws Exception { String firstHash = "firstHash"; String secondHash = "secondHash"; List<String> newContents = Arrays.asList(firstHash, secondHash); RecordServicesRuntimeException zeException = new RecordServicesRuntimeException("test"); Transaction transaction = mock(Transaction.class); when(transaction.getCollection()).thenReturn(zeCollection); doReturn(new ContentModifications(new ArrayList<String>(), newContents)).when(recordServices) .findContentsModificationsIn(metadataSchemaTypes, transaction); doThrow(zeException).when(recordServices).saveTransactionDTO(eq(transaction), eq(recordModificationImpactHandler), anyInt()); try { recordServices.saveContentsAndRecords(transaction, recordModificationImpactHandler, 0); fail("Exception expected"); } catch (Exception e) { assertThat(e).isEqualTo(zeException); verify(contentManager).silentlyMarkForDeletionIfNotReferenced(firstHash); verify(contentManager).silentlyMarkForDeletionIfNotReferenced(secondHash); } } @Test public void whenFlushingThenFlushInDao() throws Exception { recordServices.flush(); verify(recordDao).flush(); verify(eventsDao).flush(); } @Test(expected = RecordServicesRuntimeException_RecordsFlushingFailed.class) public void givenRecordDaoRuntimeExceptionWhenFlushingThenFlushInDao() throws Exception { doThrow(RecordDaoRuntimeException_RecordsFlushingFailed.class).when(recordDao).flush(); recordServices.flush(); } @Test(expected = RecordServicesRuntimeException_RecordsFlushingFailed.class) public void givenEventsDaoRuntimeExceptionWhenFlushingThenFlushInDao() throws Exception { doThrow(RecordDaoRuntimeException_RecordsFlushingFailed.class).when(eventsDao).flush(); recordServices.flush(); } }