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