001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006 007package org.fcrepo.kernel.impl.observer; 008 009import com.google.common.eventbus.EventBus; 010 011import org.fcrepo.config.AuthPropsConfig; 012import org.fcrepo.config.ServerManagedPropsMode; 013import org.fcrepo.kernel.api.Transaction; 014import org.fcrepo.kernel.api.exception.PathNotFoundException; 015import org.fcrepo.kernel.api.identifiers.FedoraId; 016import org.fcrepo.kernel.api.models.FedoraResource; 017import org.fcrepo.kernel.api.models.ResourceFactory; 018import org.fcrepo.kernel.api.observer.Event; 019import org.fcrepo.kernel.api.observer.EventType; 020import org.fcrepo.kernel.api.operations.ResourceOperation; 021import org.fcrepo.kernel.impl.operations.DeleteResourceOperationFactoryImpl; 022import org.fcrepo.kernel.impl.operations.RdfSourceOperationFactoryImpl; 023import org.hamcrest.Description; 024import org.hamcrest.Matcher; 025import org.hamcrest.TypeSafeMatcher; 026import org.junit.Before; 027import org.junit.Test; 028import org.junit.runner.RunWith; 029import org.mockito.ArgumentCaptor; 030import org.mockito.Mock; 031import org.mockito.Mockito; 032import org.mockito.junit.MockitoJUnitRunner; 033 034import java.net.URI; 035import java.net.URISyntaxException; 036import java.util.Arrays; 037import java.util.Objects; 038import java.util.Set; 039 040import static org.fcrepo.kernel.api.RdfLexicon.RDF_SOURCE; 041import static org.hamcrest.MatcherAssert.assertThat; 042import static org.hamcrest.Matchers.contains; 043import static org.hamcrest.Matchers.containsInAnyOrder; 044import static org.junit.Assert.assertEquals; 045import static org.mockito.ArgumentMatchers.any; 046import static org.mockito.ArgumentMatchers.eq; 047import static org.mockito.Mockito.times; 048import static org.mockito.Mockito.verify; 049import static org.mockito.Mockito.when; 050import static org.springframework.test.util.ReflectionTestUtils.setField; 051 052/** 053 * @author pwinckles 054 */ 055@RunWith(MockitoJUnitRunner.class) 056public class EventAccumulatorImplTest { 057 058 private static final String TX_ID = "tx1"; 059 private static final String BASE_URL = "http://localhost/rest"; 060 private static final String USER_AGENT = "user-agent"; 061 private static final String USER = "user"; 062 063 private static final URI CONTAINER_TYPE = uri("http://www.w3.org/ns/ldp#Container"); 064 private static final URI RESOURCE_TYPE = uri("http://fedora.info/definitions/v4/repository#Resource"); 065 private static final URI RDF_TYPE = uri("http://www.w3.org/ns/ldp#RDFSource"); 066 067 private EventAccumulatorImpl accumulator; 068 069 @Mock 070 private ResourceFactory resourceFactory; 071 072 @Mock 073 private EventBus eventBus; 074 075 private Transaction transaction; 076 077 private ArgumentCaptor<Event> eventCaptor; 078 079 private AuthPropsConfig authPropsConfig; 080 081 @Before 082 public void setup() { 083 authPropsConfig = new AuthPropsConfig(); 084 accumulator = new EventAccumulatorImpl(); 085 transaction = mockTransaction(TX_ID); 086 setField(accumulator, "resourceFactory", resourceFactory); 087 setField(accumulator, "eventBus", eventBus); 088 setField(accumulator, "authPropsConfig", authPropsConfig); 089 eventCaptor = ArgumentCaptor.forClass(Event.class); 090 } 091 092 @Test 093 public void emitEventsWhenEventsOnTransactionNoMerge() throws PathNotFoundException { 094 final var fId1 = FedoraId.create("/test/1"); 095 final var fId2 = FedoraId.create("/test/2"); 096 097 final var op1 = createOp(fId1); 098 final var op2 = updateOp(fId2); 099 100 accumulator.recordEventForOperation(transaction, fId1, op1); 101 accumulator.recordEventForOperation(transaction, fId2, op2); 102 103 expectResource(fId1, CONTAINER_TYPE); 104 expectResource(fId2, CONTAINER_TYPE, RESOURCE_TYPE); 105 106 accumulator.emitEvents(transaction, BASE_URL, USER_AGENT); 107 108 verify(eventBus, times(2)).post(eventCaptor.capture()); 109 110 final var events = eventCaptor.getAllValues(); 111 112 assertThat(events, containsInAnyOrder( 113 defaultEvent(fId1, Set.of(EventType.RESOURCE_CREATION), Set.of(CONTAINER_TYPE.toString())), 114 defaultEvent(fId2, Set.of(EventType.RESOURCE_MODIFICATION), 115 Set.of(CONTAINER_TYPE.toString(), RESOURCE_TYPE.toString())) 116 )); 117 } 118 119 @Test 120 public void emitEventsWhenEventsOnTransactionWithMerge() throws PathNotFoundException { 121 final var fId1 = FedoraId.create("/test/1"); 122 final var fId2 = FedoraId.create("/test/2"); 123 124 final var op1 = createOp(fId1); 125 final var op2 = updateOp(fId2); 126 final var op3 = updateOp(fId1); 127 128 accumulator.recordEventForOperation(transaction, fId1, op1); 129 accumulator.recordEventForOperation(transaction, fId2, op2); 130 accumulator.recordEventForOperation(transaction, fId1, op3); 131 132 expectResource(fId1, CONTAINER_TYPE, RDF_TYPE); 133 expectResource(fId2, CONTAINER_TYPE, RESOURCE_TYPE); 134 135 accumulator.emitEvents(transaction, BASE_URL, USER_AGENT); 136 137 verify(eventBus, times(2)).post(eventCaptor.capture()); 138 139 final var events = eventCaptor.getAllValues(); 140 141 assertThat(events, containsInAnyOrder( 142 defaultEvent(fId1, Set.of(EventType.RESOURCE_CREATION, EventType.RESOURCE_MODIFICATION), 143 Set.of(CONTAINER_TYPE.toString(), RDF_TYPE.toString())), 144 defaultEvent(fId2, Set.of(EventType.RESOURCE_MODIFICATION), 145 Set.of(CONTAINER_TYPE.toString(), RESOURCE_TYPE.toString())) 146 )); 147 } 148 149 @Test 150 public void onlyEmitEventsForSameSpecifiedTransaction() throws PathNotFoundException { 151 final var fId1 = FedoraId.create("/test/1"); 152 final var fId2 = FedoraId.create("/test/2"); 153 154 final var op1 = createOp(fId1); 155 final var op2 = updateOp(fId2); 156 157 final Transaction transaction2 = mockTransaction("tx2"); 158 159 accumulator.recordEventForOperation(transaction, fId1, op1); 160 accumulator.recordEventForOperation(transaction2, fId2, op2); 161 162 expectResource(fId2, CONTAINER_TYPE); 163 164 accumulator.emitEvents(transaction2, BASE_URL, USER_AGENT); 165 166 verify(eventBus, times(1)).post(eventCaptor.capture()); 167 168 final var events = eventCaptor.getAllValues(); 169 170 assertThat(events, contains( 171 defaultEvent(fId2, Set.of(EventType.RESOURCE_MODIFICATION), 172 Set.of(CONTAINER_TYPE.toString())) 173 )); 174 } 175 176 @Test 177 public void doNothingWhenTransactionHasNoEvents() throws PathNotFoundException { 178 final var fId1 = FedoraId.create("/test/1"); 179 final var fId2 = FedoraId.create("/test/2"); 180 181 final var op1 = createOp(fId1); 182 final var op2 = updateOp(fId2); 183 184 final Transaction transaction2 = mockTransaction("tx2"); 185 final Transaction transaction3 = mockTransaction("tx3"); 186 187 accumulator.recordEventForOperation(transaction, fId1, op1); 188 accumulator.recordEventForOperation(transaction2, fId2, op2); 189 190 expectResource(fId2, CONTAINER_TYPE); 191 192 accumulator.emitEvents(transaction3, BASE_URL, USER_AGENT); 193 194 verify(eventBus, times(0)).post(eventCaptor.capture()); 195 196 final var events = eventCaptor.getAllValues(); 197 198 assertEquals(0, events.size()); 199 } 200 201 @Test 202 public void clearTransactionEventsWhenCleared() throws PathNotFoundException { 203 final var fId1 = FedoraId.create("/test/1"); 204 final var fId2 = FedoraId.create("/test/2"); 205 206 final var op1 = createOp(fId1); 207 final var op2 = updateOp(fId2); 208 final var op3 = updateOp(fId1); 209 210 accumulator.recordEventForOperation(transaction, fId1, op1); 211 accumulator.recordEventForOperation(transaction, fId2, op2); 212 accumulator.recordEventForOperation(transaction, fId1, op3); 213 214 expectResource(fId1, CONTAINER_TYPE, RDF_TYPE); 215 expectResource(fId2, CONTAINER_TYPE, RESOURCE_TYPE); 216 217 accumulator.clearEvents(transaction); 218 219 accumulator.emitEvents(transaction, BASE_URL, USER_AGENT); 220 221 verify(eventBus, times(0)).post(eventCaptor.capture()); 222 223 final var events = eventCaptor.getAllValues(); 224 assertEquals(0, events.size()); 225 } 226 227 @Test 228 public void nonDefaultValues() throws PathNotFoundException { 229 final var tx = "tx4"; 230 final Transaction transaction4 = mockTransaction(tx); 231 final var url = "http://example.com/rest"; 232 final var agent = "me"; 233 234 final var fId1 = FedoraId.create("/example/1"); 235 final var fId2 = FedoraId.create("/example/2"); 236 237 final var op1 = createOp(fId1); 238 final var op2 = updateOp(fId2); 239 final var op3 = deleteOp(fId1); 240 241 accumulator.recordEventForOperation(transaction4, fId1, op1); 242 accumulator.recordEventForOperation(transaction4, fId2, op2); 243 accumulator.recordEventForOperation(transaction4, fId1, op3); 244 245 expectResource(fId1, RDF_TYPE); 246 expectResource(fId2, RESOURCE_TYPE); 247 248 accumulator.emitEvents(transaction4, url, agent); 249 250 verify(eventBus, times(2)).post(eventCaptor.capture()); 251 252 final var events = eventCaptor.getAllValues(); 253 254 assertThat(events, containsInAnyOrder( 255 event(fId1, Set.of(EventType.RESOURCE_CREATION, EventType.RESOURCE_DELETION), 256 Set.of(RDF_TYPE.toString()), url, agent), 257 event(fId2, Set.of(EventType.RESOURCE_MODIFICATION), 258 Set.of(RESOURCE_TYPE.toString()), url, agent) 259 )); 260 } 261 262 @Test 263 public void doNotSetResourceTypesWhenCannotLoad() throws PathNotFoundException { 264 final var fId1 = FedoraId.create("/test/1"); 265 final var fId2 = FedoraId.create("/test/2"); 266 final var fId3 = FedoraId.create("/test/3"); 267 268 final var op1 = createOp(fId1); 269 final var op2 = deleteOp(fId2); 270 final var op3 = updateOp(fId3); 271 272 accumulator.recordEventForOperation(transaction, fId1, op1); 273 accumulator.recordEventForOperation(transaction, fId2, op2); 274 accumulator.recordEventForOperation(transaction, fId3, op3); 275 276 expectResource(fId1, CONTAINER_TYPE); 277 when(resourceFactory.getResource(any(Transaction.class), eq(fId2))).thenThrow(new PathNotFoundException("not " + 278 "found")); 279 expectResource(fId3, RESOURCE_TYPE); 280 281 accumulator.emitEvents(transaction, BASE_URL, USER_AGENT); 282 283 verify(eventBus, times(3)).post(eventCaptor.capture()); 284 285 final var events = eventCaptor.getAllValues(); 286 287 assertThat(events, containsInAnyOrder( 288 defaultEvent(fId1, Set.of(EventType.RESOURCE_CREATION), 289 Set.of(CONTAINER_TYPE.toString())), 290 defaultEvent(fId2, Set.of(EventType.RESOURCE_DELETION), 291 Set.of()), 292 defaultEvent(fId3, Set.of(EventType.RESOURCE_MODIFICATION), 293 Set.of(RESOURCE_TYPE.toString())) 294 )); 295 } 296 297 private ResourceOperation createOp(final FedoraId fedoraId) { 298 return new RdfSourceOperationFactoryImpl().createBuilder(transaction, fedoraId, RDF_SOURCE.toString(), 299 ServerManagedPropsMode.RELAXED) 300 .userPrincipal(USER) 301 .build(); 302 } 303 304 private ResourceOperation updateOp(final FedoraId fedoraId) { 305 return new RdfSourceOperationFactoryImpl().updateBuilder(transaction, fedoraId, ServerManagedPropsMode.RELAXED) 306 .userPrincipal(USER) 307 .build(); 308 } 309 310 private ResourceOperation deleteOp(final FedoraId fedoraId) { 311 return new DeleteResourceOperationFactoryImpl().deleteBuilder(transaction, fedoraId) 312 .userPrincipal(USER) 313 .build(); 314 } 315 316 private void expectResource(final FedoraId fedoraId, final URI... types) throws PathNotFoundException { 317 final var resource = mockResource(types); 318 when(resourceFactory.getResource(any(Transaction.class), eq(fedoraId))).thenReturn(resource); 319 } 320 321 private FedoraResource mockResource(final URI... types) { 322 final var resource = Mockito.mock(FedoraResource.class); 323 when(resource.getTypes()).thenReturn(Arrays.asList(types)); 324 return resource; 325 } 326 327 private static URI uri(final String uri) { 328 try { 329 return new URI(uri); 330 } catch (final URISyntaxException e) { 331 throw new RuntimeException(e); 332 } 333 } 334 335 private static Matcher<Event> defaultEvent(final FedoraId fedoraId, 336 final Set<EventType> eventTypes, 337 final Set<String> resourceTypes) { 338 return event(fedoraId, eventTypes, resourceTypes, BASE_URL, USER_AGENT); 339 } 340 341 private static Matcher<Event> event(final FedoraId fedoraId, 342 final Set<EventType> eventTypes, 343 final Set<String> resourceTypes, 344 final String baseUrl, 345 final String userAgent) { 346 return new TypeSafeMatcher<>() { 347 @Override 348 protected boolean matchesSafely(final Event item) { 349 if (! Objects.equals(item.getFedoraId(), fedoraId)) { 350 return false; 351 } else if (! Objects.equals(item.getTypes(), eventTypes)) { 352 return false; 353 } else if (! Objects.equals(item.getResourceTypes(), resourceTypes)) { 354 return false; 355 } else if (! Objects.equals(item.getBaseUrl(), baseUrl)) { 356 return false; 357 } else if (! Objects.equals(item.getUserAgent(), userAgent)) { 358 return false; 359 } 360 return true; 361 } 362 363 @Override 364 public void describeTo(final Description description) { 365 description.appendText("fedoraId=").appendValue(fedoraId) 366 .appendText(", eventTypes=").appendValue(eventTypes) 367 .appendText(", resourceTyps=").appendValue(resourceTypes) 368 .appendText(", baseUrl=").appendValue(baseUrl) 369 .appendText(", userAgent=").appendValue(userAgent); 370 } 371 }; 372 } 373 374 /** 375 * Create a mock transaction. 376 * @param transactionId the id of the transaction 377 * @return the mock transaction. 378 */ 379 private static Transaction mockTransaction(final String transactionId) { 380 final var transaction = Mockito.mock(Transaction.class); 381 when(transaction.getId()).thenReturn(transactionId); 382 return transaction; 383 } 384}